Using Zend Framework 2
Using Zend Framework 2
Using Zend Framework 2
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools
and many iterations to get reader feedback, pivot until you have the right book and build
traction once you do.
2013 - 2014 Oleg Krivtsov
To my father who assembled my first computer and shown me how to write a simple program.
To my mother who shown me how to overcome lifes obstacles and become a winner.
Contents
Preface . . . . . . . . . . . . .
About this Leanpub Book . .
Why to Read this Book? . .
Zend Framework Explained
See ZF2 Wider . . . . . . . .
ZF2 Book for Beginners . . .
Structure of the Book . . . .
Learn ZF2 by Example . . .
Book Site . . . . . . . . . . .
Book Reviews . . . . . . . .
Testimonials . . . . . . . . .
Your Feedback . . . . . . . .
Affiliate Program . . . . . .
Acknowledgements . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
i
i
i
i
ii
ii
ii
iii
iv
iv
iv
v
v
vi
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
2
3
5
6
6
8
8
9
10
10
11
15
16
16
16
17
17
17
17
17
CONTENTS
17
21
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
22
22
23
25
27
29
30
32
34
35
36
36
37
37
38
39
40
41
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
42
42
44
46
47
49
50
51
52
53
55
55
56
57
58
58
59
60
61
62
62
64
65
65
CONTENTS
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
67
67
68
69
70
4. Model-View-Controller . . . . . . . . . . . . . . .
4.1 Get the Hello World Example from GitHub . .
4.2 Separating Business Logic from Presentation .
4.3 Controllers . . . . . . . . . . . . . . . . . . . .
4.3.1 Base Controller Class . . . . . . . . . . .
4.3.2 Retrieving Data from HTTP Request . .
4.3.3 Retrieving GET or POST Variables . . .
4.3.4 Putting Data to HTTP Response . . . . .
4.4 Variable Containers . . . . . . . . . . . . . . .
4.5 Controller Registration . . . . . . . . . . . . .
4.6 When to Create a New Controller? . . . . . . .
4.7 Controller Plugins . . . . . . . . . . . . . . . .
4.7.1 Writing Own Controller Plugin . . . . .
4.8 Views . . . . . . . . . . . . . . . . . . . . . .
4.9 View Helpers . . . . . . . . . . . . . . . . . .
4.10 View Template Names & View Resolver . . . .
4.11 Disabling the View Rendering . . . . . . . . .
4.12 Error Pages . . . . . . . . . . . . . . . . . . .
4.13 Models . . . . . . . . . . . . . . . . . . . . . .
4.14 Model Types . . . . . . . . . . . . . . . . . . .
4.14.1 Entities . . . . . . . . . . . . . . . . . .
4.14.2 Value Objects . . . . . . . . . . . . . . .
4.14.3 Services . . . . . . . . . . . . . . . . . .
4.14.4 Factories . . . . . . . . . . . . . . . . .
4.14.5 Repositories . . . . . . . . . . . . . . . .
4.15 Determining the Correct Model Type . . . . .
4.16 Skinny Controllers, Fat Models, Simple Views .
4.16.1 Skinny Controllers . . . . . . . . . . . .
4.16.2 Fat Models . . . . . . . . . . . . . . . .
4.16.3 Simple View Templates . . . . . . . . . .
4.17 Summary . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
72
72
73
75
77
78
79
80
80
82
83
83
84
85
89
91
93
95
97
97
98
99
100
102
102
103
104
104
105
106
106
5. URL Routing . . . . . . . . . . . . . . . . .
5.1 URL Structure . . . . . . . . . . . . . .
5.2 Route Types . . . . . . . . . . . . . . .
5.3 Combining Route Types . . . . . . . .
5.3.1 Simple Route Stack . . . . . . . .
5.3.2 Tree Route Stack . . . . . . . . .
5.4 Routing Configuration . . . . . . . . .
5.4.1 Configuration for Simple Routes .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
107
107
108
109
110
110
111
112
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
CONTENTS
5.5
5.6
5.7
5.8
5.9
5.10
5.11
5.12
5.13
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
112
113
115
116
120
122
124
126
127
128
128
129
130
131
131
132
133
133
133
143
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
144
144
146
150
154
155
156
158
160
164
167
168
169
171
177
179
181
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
183
183
184
186
187
188
189
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
CONTENTS
7.4
7.5
7.6
7.7
7.8
7.9
7.10
7.11
7.12
7.13
7.14
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
191
194
195
196
198
201
201
203
204
208
208
209
210
210
211
211
214
218
222
222
223
224
225
225
226
227
227
229
231
232
232
235
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
236
236
236
237
239
240
242
242
243
243
244
244
244
CONTENTS
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
244
247
248
249
249
249
250
251
251
253
254
254
255
256
257
257
259
260
261
264
267
268
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
270
270
270
271
273
274
275
278
279
280
280
280
280
281
283
284
285
286
286
288
289
290
291
CONTENTS
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
292
293
295
299
300
302
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
303
303
303
305
307
307
308
310
311
311
312
313
314
316
317
319
324
325
328
330
332
333
335
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
336
336
336
337
338
339
342
344
346
348
349
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
350
350
352
353
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
CONTENTS
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
354
356
357
358
360
361
363
363
365
366
368
368
371
375
375
378
380
381
382
383
385
386
390
393
396
397
398
402
403
404
405
407
409
410
414
417
418
420
421
422
424
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
425
425
426
428
CONTENTS
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
428
430
430
431
432
434
434
435
435
438
439
440
441
442
442
444
444
445
448
448
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
450
450
452
453
454
455
456
457
458
460
463
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
464
464
466
467
468
468
469
470
471
471
472
473
474
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
CONTENTS
Pagination . . . . . .
Buttons & Glyphicons
Customizing Bootstrap . .
Summary . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
474
475
477
478
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
479
479
479
480
481
482
482
482
484
485
Preface
About this Leanpub Book
Thanks to Leanpub, you have a chance to obtain this book at a lower cost and give your feedback
to the author. This makes it possible for the author to immediately correct mistakes and enhance
the places in text that you point to, thus creating a high-quality product.
You will receive all newer versions of this book for free as they appear.
Preface
ii
iii
Preface
https://github.com/olegkrivtsov/using-zend-framework-2-book
iv
Preface
using-zend-framework-2-book
blog
helloworld
formdemo
...
Book Site
The Using Zend Framework 2 book has a dedicated web site using-zend-framework-2book.com. This is the central place where you can find all the information about the book:
intro videos,
tutorials,
code examples,
reader reviews & feedback,
announcements,
and more.
Book Reviews
Richard Holloway: This will likely improve your overall understanding of modern PHP
Richard Holloway is an organiser of PHP Hampshire, which is a recognised PHP user group:
Many people struggle to get into Zend Framework 2 but this book does a good job of taking you
over that initial steep learning curve and providing enough information to get you started on
building websites.
The complete review is available by this link.
Testimonials
Below, there are some selected testimonials from satisfied readers of the book:
Im a very satisfied reader of your book (using zend framework 2: it details many important
notions, but it never miss to give the big picture: great work! Francesco
Ive recently bought your book Using Zend Framework 2 and I think this is the best available
resource to get started with ZF2. Janusz K
I purchased your book on Zend framework 2 some days ago and I thought i should congratulate
you for your amazing work. I tried another books and methods to learn zf2, but definitely your
book is the only that works for me.
http://using-zend-framework-2-book.com
http://phphants.co.uk
http://richardjh.org/blog/book-review-using-zend-framework-2/
Preface
Zf2 is something complex to me and your book is making it easier. I really like the detailed
explanations of the concepts and examples you use. Welington*
I am one of (hopefully many) people who bought and read your using ZF2 book. [] Your book
taught me not only many new concepts, but also why these concepts came to be and (as a personal
comfort to me) that almost half of these new features (or rather: ways of thinking) were things I
was already doing, albeit in some other, non-object oriented way; I just never realised it. Having
things explained by someone who obviously knows what he is talking about was a great help to
me, and while I have yet to reach any important milestone, I feel I understand what I have to do
much better now and I am much more confident that I will eventually successfully refresh my
hopelessly outdated projects. J.B.
Your Feedback
Thank you for reading this book and helping to make it better. You are encouraged to point
out errors, make suggestions and critical remarks. You can write the author a message through
the dedicated Forum. Alternatively, you can contact the author through his E-mail address
([email protected]). Your feedback is highly appreciated.
Affiliate Program
Now the Using Zend Framework 2 book is part of the Leanpub Affiliate Program. This means
that anyone can earn 50% of profit for advertising the book on his/her web site. So, if you like this
book and want to earn money by promoting it on your web page or blog, feel free to participate!
How does this Work?
1. Create an account at Leanpub site.
2. Go to the Affiliate page in your account.
3. Look for the Using Zend Framework 2 book in the list of books participating in the
affiliate program.
4. Click the Copy affiliate URL button.
5. Paste the affiliate URL on your web site.
6. When someone buys the book after going to the page using your affiliate code, you get
50% of the minimum price of the book.
Affiliate Link
https://leanpub.com/using-zend-framework-2/feedback
http://blog.leanpub.com/2014/03/introducing-the-leanpub-affiliate-program.html
https://leanpub.com/
Preface
vi
You can read more about the Leanpub affiliate program terms on this page.
Acknowledgements
Thanks to Edu Torres, a 2D artist from Spain, for making the cover and an illustration for this
book, and for making a design for the books web site. Also thanks to Moriancumer Richard Uy
and Charles Naylor for helping the author to find and fix the mistakes in the text.
The author would like to thank Richard Holloway (an organiser of PHP Hampshire, which is
a recognised PHP user group in South England) for reviewing the book. Richards review is
really useful for determining the proper development direction for this book.
http://blog.leanpub.com/2014/03/introducing-the-leanpub-affiliate-program.html
http://phphants.co.uk
http://richardjh.org/blog/book-review-using-zend-framework-2/
Write secure web sites with ZF2-provided components like form input filters and validators, HTML output escapers and cryptography algorithms, human check (Captcha) and
Cross-Site Request Forgery (CSRF) form elements.
Figure 1.1. Zend Framework sits on top of PHP and contains reusable components for building your web site
1.2 License
Zend Framework 2 is licensed under BSD-like license, allowing you to use it in both commercial
and free applications. You can even modify the library code and release it under another name.
The only thing you cannot do is to remove the copyright notice from the code. If you use Zend
Framework 2, it is also recommended that you mention about it in your sites documentation or
on About page.
Below, the Zend Framework 2 license text is presented. As you can see, it is rather short.
BNP Paribas Banque BNP Paribas is a French bank and financial services company,
European leader in global banking and financial services and is one of the six strongest
banks in the world according to the agency Standard & Poors .
BNP Paribas From Wikipedia, the free encyclopedia http://en.wikipedia.org/wiki/BNP_Paribas
Date
Version
ZF 2.3.0
ZF 2.2.5
ZF 2.2.4
ZF 2.2.3
ZF 2.2.2
ZF 2.2.1
ZF 2.2.0 Stable
ZF 2.2.0rc3
ZF 2.2.0rc2
ZF 2.2.0rc1
ZF 2.1.5
ZF 2.1.5
http://framework.zend.com/blog
Date
Version
ZF 2.1.4
ZF 2.1.3
ZF 2.1.2
ZF 2.1.1
ZF 2.1.0 Stable
ZF 2.0.7
ZF 2.0.6
ZF 2.0.5
ZF 2.0.4
ZF 2.0.3
ZF 2.0.2
ZF 2.0.1
ZF 2.0.0 Stable
As you can see from the table above, Zend Framework 2 is being constantly developed and
updated. Its developers listen to user communitys feedback and strive to keep the framework
well polished and ready for use in production systems. For the detailed list of changes between
the versions of ZF2, you can refer to the Changelog page.
1.5 Distributions
You can download the source code of Zend Framework 2 from the official site (presented in
figure 1.4) to become familiar with its structure and components.
ZF2 can be downloaded in two types: full and minimum. A full-size archive contains a complete
set of components plus demos; its size is about 3 Mb. Mimimum-size distribution contains library
components only, and its size is 3 Mb (also !).
In most cases you wont need to download the code of Zend Framework 2 manually.
Instead, you will install it with Composer dependency manager. We will become
familiar with Composer later in Chapter 2.
Documentation. Documentation for ZF2 is located by this address. It includes beginners tutorial,
programmers manual, and API reference (API stands for Application Programming Interface).
Community Forums. Zend Framework 2 has dedicated user groups all over the world. The list of
groups can be found on this page.
Webinars are video tutorials covering various ZF2 features. Complete list of webinars can be
http://framework.zend.com/learn/
http://framework.zend.com/participate/user-groups
ZF2 utilizes URL rewriting extension for redirecting web-users to entry script of your site
(you have to enable Apaches mod_rewrite module.) You may also need to install some PHP
extensions, like memory caching extension, to improve ZF2 performance. This can be a difficulty
when using a shared hosting and requires that you have admin rights on your server.
So, if you are planning to use ZF2 on a shared web hosting, think twice. The best server to install
ZF2 on is a server with the latest version of PHP and with shell access to be able to execute
Composer, install PHP extensions and provide an ability to schedule console PHP scripts by
cron.
If your company manages its own server infrastructure and can afford upgrading PHP version
to the latest one, you can install ZF2 on your private server.
An acceptable alternative is installing a ZF2-based web application to a cloud-based hosting
service, like Amazon Web Services. Amazon provides Linux server instances as a part of EC2
service. EC2 is rather cheap, and it provides a free usage tier letting you try it for free for one
year.
1.9 Security
Zend Framework 2 follows the best practices to provide you with a secure code base for your
web sites. ZF2 creators release security bug fixes on a regular basis. You can incorporate those
fixes with a single command through Composer dependency manager.
ZF2 provides many tools allowing to make your web site secure:
Routing allows to define strict rules on how an acceptable page URL should look like. If
a site user enters an invalid URL in a web browsers navigation bar, he is automatically
redirected to an error page.
Access control lists and Role-Based Access Control (RBAC) allow to define flexible rules
for granting or denying access to certain resources of your web site. For example, an
anonymous user would have access to your index page only, authenticated users would
have access to their profile page, and the administrator user would have access to site
management panel.
Form validators and filters ensure that no unwanted data is collected through web forms.
Filters, for example, allow to trim strings or strip HTML tags. Validators are used to
check that the data that had been submitted through a form conforms to certain rules.
For example, E-mail validator checks that an E-mail field contains valid E-mail address,
and if not, raises an error forcing the site user to correct the input error.
Captcha and CSRF (Cross-Site Request Forgery) form elements are used for human checks
and hacker attack prevention, respectively.
Escaper component allows to strip unwanted HTML tags from data outputted to site pages.
Cryptography support allows you to store your sensitive data (e.g. credentials) encrypted.
http://aws.amazon.com/
http://aws.amazon.com/free/
10
1.10 Performance
ZF2 creators have claimed to do a great job to improve performance of the ZF2 comparing to the
first version of the framework.
Lazy class autoloading. Classes are loaded once needed. You dont have to write require_once
for each class you want to load. Instead, the framework automatically discovers your classes
using the autoloader feature. Autoloader uses either class map or class naming conventions to
find and load the needed class.
Efficient plugin loading. In ZF2, plugin classes are instantiated only when they really need to.
This is achieved through service manager (the central repository for services of your application).
Support of caching. PHP has several caching extensions (like APC or Memcache) that can be used
to speed-up ZF2-based web sites. Caching saves frequently used data to memory to speed-up data
retrieval. For example, a Zend Framework 2 application consists of many files which require time
for PHP interpreter to process the files each time you open the page in the browser. You can use
APC extension to cache precompiled PHP opcodes to speed up your site. Additionally, you can
use the ZF2s event manager to listen to dispatching events, and cache HTML response data with
Memcache extension.
Are there any benchmark tests of ZF2 performance?
As per the authors knowledge, currently, there are no reliable benchmark tests that
would allow to compare ZF2 performance with performance of other frameworks.
11
Aspect Oriented Design pattern. In ZF2, everything is event-driven. When a site user
requests a page, an event is generated (triggered). A listener (or observer) can catch event
and do something with it. For example, a router service parses the URL and determines
what controller class to call. When the event finally reaches the page renderer, an HTTP
response is generated and the user sees the web page.
Singleton pattern. In ZF2, there is the service manager object which is the centralized
registry of all services available in the application. Each service exists in a single instance
only.
Strategy pattern. While browsing ZF2 documentation or source code, youll encounter the
word strategy for sure. A strategy is just a class encapsulating some algorithm. And you
can use different algorithms based on some condition. For example, the page renderer has
several rendering strategies, making it possible to render web pages differently based on
Accept HTTP header (the renderer can generate an HTML page, a JSON response, an RSS
feed etc.)
Adapter pattern. Adapters allow to adapt a generic class to concrete use case. For example,
Zend\Db component provides access to database in a generic way. Internally, it uses
adapters for each supported database (SQLite, MySQL, PostgreSQL and so on.)
Factory pattern. You can create an instance of a class using the new operator. Or you can
create it with a factory. A factory is just a class encapsulating creation of other objects.
Factories are useful, because they simplify dependency injection - you can provide a
generic factory interface instead of hard-coding the concrete class name. This simplifies
the testing of your model and controller classes.
1.12 Components
ZF2 developers believe that the framework should be a set of decoupled components with
minimum dependencies on each other. This is how ZF2 is organized.
The idea was to let you use some selected ZF2 components alone, even if you write your site with
another framework. This becomes even easier, keeping in mind that each component of ZF2 is
a Composer-installable package, so you can easily install any ZF2-component together with its
dependencies through a single command.
The table 1.2 lists ZF2 components with their brief description. As you can see from the table,
we can divide all ZF2 components into the following groups :
Core Components. These components are used (either explicitly or implicitly) in almost any
web application. They provide the base functionality for class auto-loading, for triggering
events and listening to them, for loading modules, for organizing the code within a module
in conformance to the Model-View-Controller pattern, for creating console commands and
more.
Web Forms. Forms are the way of collecting user-entered data on web pages. A form
usually consists of elements (input fields, labels, etc). For checking and filtering the userentered data, filters and validators are utilized.
These component groups are not an official classification, but the authors personal point of view.
12
User Management. This important group includes components for providing user authentication, authorization and access control. Internally, these are based on the PHP feature
called sessions.
Presentation Utilities. In this group, we can put components implementing useful web page
elements: navigation bars, progress bars, etc.
Persistence. This group contains components whose purpose is to convert in-memory data
into formats storable on a disk media (XML, JSON, a database, etc.), and vice-versa.
Testing and Debugging. In this (small) group, there are several components for logging,
testing and debugging your web site.
Web Services. This group contains components that implement various protocols for
accessing your web site programmatically (e.g. RSS, XML RPC, SOAP and so on).
Mail. Useful components for composing E-mail messages and sending them with different
transports.
Miscellaneous. Various components that cannot be put in any other group.
Component Name
Description
Core Components
Zend\Cache
Zend\Code
Zend\Console
Zend\Di
Zend\EventManager
Zend\Http
Zend\Loader
Zend\ModuleManager
Zend\Mvc
Zend\ServiceManager
13
Component Name
Description
any point of the web site. This component is covered in Chapter 3.
Zend\Stdlib
Zend\View
Zend\Uri
Persistence
Zend\Dom
Zend\Db
Zend\Json
Zend\Serializer
User Management
Zend\Authentication
Zend\Permissions
Zend\Session
Presentation Utilities
Zend\Barcode
Zend\Captcha
Zend\Navigation
Zend\Paginator
Zend\ProgressBar
Zend\Escaper
Smart class for escaping text output. Used to secure web site views.
Zend\Tag
14
Component Name
Description
Zend\Log
Zend\Test
Web Forms
Zend\Filter
Provides a set of commonly needed data filters, like string trimmer. This
component is covered in Chapter 8.
Zend\Form
Zend\InputFilter
Zend\Validator
Web Services
Zend\Feed
Zend\Ldap
Zend\Server
Zend\Soap
Zend\XmlRpc
Mail
Zend\Mail
Zend\Mime
Miscellaneous
Zend\Config
Zend\Crypt
Zend\File
Zend\I18n
Zend\Math
Zend\Memory
15
Component Name
Description
works in limited memory mode.
Zend\Text
Zend\Version
Description
ZendService\Akismet
ZendService\Amazon
ZendService\AppleApns
Provides a client for the Apple Push Notification Service (APNs for
short), which is a service for propagating information to iOS and
Mac OS X devices.
ZendService\Audioscrobbler
ZendService\Delicious
ZendService\DeveloperGarden
ZendService\Flickr
ZendService\Google\Gcm
ZendService\LiveDocx
ZendService\Nirvanix
http://akismet.com/
http://aws.amazon.com/
http://www.audioscrobbler.net/
https://delicious.com/
http://www.flickr.com/
16
Component Name
Description
ZendService\Rackspace
ZendService\ReCaptcha
ZendService\SlideShare
ZendService\StrikeIron
ZendService\Technorati
ZendService\Twitter
ZendService\Windows Azure
Because the API to above mentioned web resources may be changed by their vendors
without a notice, those components are not part of the core Zend Framework 2
distribution. By the same reason, those components are not discussed deeply in this
book.
1.14.2 ZFTool
In Zend Framework 1, you used ZFTool for creating the application, adding layouts and
controllers. In ZF2, you create your new applications by downloading Zend Skeleton Application
available on GitHub. By the way, in ZF2 you can install a component called ZFTool, and it can
also create the skeleton application or a module for you.
http://www.google.com/recaptcha
http://www.slideshare.net/
http://www.strikeiron.com/
http://technorati.com/
http://twitter.com
http://www.windowsazure.com/
17
1.14.3 Modules
In Zend Framework 1, your application was monolithic (although there was a concept of module).
In ZF2, everything is a module. The skeleton application has single Application module by
default. Each module may contain configuration, models, views, controllers and the assets (e.g.
database tables, files etc.) A module can call classes from other modules. You can install thirdparty modules and reuse your own modules across applications.
1.14.5 Namespaces
In ZF1, you worked with long underscore-separated class names like Zend_Controller_Action.
In ZF2, PHP namespaces are used, so instead youll have something like Zend\MVC\Controller\AbstractActionC
which can be easier to type with auto-completion feature and easier to understand. Namespaces
allow to define short class names (aliases) and use them instead of full names. By convention,
namespaces are mapped to directory structure, making it easier to perform class autoloading.
1.14.6 Configuration
In ZF1, you had an application-level INI config file. In ZF2, each module has its configuration
file in a form of PHP array. At application level, module configurations are finally merged into
a single large nested PHP array.
18
frameworks in some way, we can use Google Trends site, which allows to track count of a
keyword search queries over time. For example, if you enter Zend Framework, CakePHP, Yii,
CodeIgniter, Symfony into the query field, you will get the graph as shown in figure 1.5.
As you can see from the graph, Zend Framework (blue line) has reached its popularity peak
by 2010, and since then it has slowly lost its popularity. However, ZF is still one of the strong
players on the market. On the other hand, Cake PHP, Symfony, CodeIgniter and Yii framework
are becoming highly popular nowadays.
Lets also look at the relative popularity of ZF1 and ZF2 by typing Zend Framework, Zend
Framework 2 into the search query field. The result is shown in figure 1.6.
As we can see, Zend Framework 2 (the red line) was released not so long ago, and has yet to
become popular. The author believes that ZF2 has all the necessary qualities to become popular
over time.
http://www.google.ru/trends/
19
Figure 1.6. Popularity of Zend Framework 2 comparing to the first version. Powered by Google Trends
If you are familiar with one of the above mentioned frameworks, in table 1.4 you can find the
comparison of features provided by those PHP frameworks. Capabilities of Zend Framework 2
are marked with bold.
Table 1.4. Comparison of Features provided by PHP frameworks
Feature
ZF2
Symfony 2
Cake PHP
CodeIgniter
Yii
Current
version
Distribution
archive size
2.2.1
2.3.1
2.3.6
2.1.3
1.1.13
3 Mb
4.4 Mb
2.0 Mb
2.2 Mb
3.9 Mb
Installation
Composer
Composer
Archive
Archive
Archive
Compatibility
with
shared
hostings
Bad (requires
SSH and
vhosts)
Bad (requires
SSH and
vhosts)
Good
Good
Good
Monolithic or
componentbased?
Components
Components
Components
Components
Monolithic
Prefer
conventions
or configs?
Configuration Configuration
Conventions
Conventions
Conventions
Database
access
pattern
Data Mapper
Active Record
Traditional
Active Record,
or
Active Record
PDO
Data Mapper
(Doctrine/ORM),(Doctrine/ORM)
Table
Gateway,
Row
Gateway
20
Feature
ZF2
Symfony 2
Cake PHP
CodeIgniter
Yii
Database
Yes
(Doctrineprovided)
Yes (Doctrine-
Yes
Yes
Yes
Any
Any
Blueprint CSS
Bootstrap
Bootstrap
View
Template
Language
Any you
want, or
none at all
Twig
Smarty/Twig
Any you
want,
or none
None or Prado
Unit testing
Yes
(PHPUnit)
Yes (PHPUnit)
Yes (PHPUnit)
Yes (PHPUnit)
Yes (PHPUnit-
Yes
(PHPUnit)
Yes (PHPUnit)
Yes
No
Yes (Selenium)
Yes
(Doctrineprovided)
Yes (Doctrine
Yes
No
Yes
migrations
CSS
Framework
support
Functional
testing
Database
fixtures
provided)
(recommended)
based)
bundle)
21
the code you write. For testing the database functionality, you can use Doctrine-provided
fixture mechanism.
1.16 Summary
A PHP framework is a library, giving you the code base and defining consistent ways of creating
web applications. Zend Framework 2 is a modern web development framework created by
the Zend Company, the vendor of PHP language. It provides the developers with outstanding
capabilities for building scalable and secure web sites. ZF2 is licensed under BSD-like license and
can be used for free in both commercial and open-source applications.
ZF2 is updated frequently, making your sites more resistant to vulnerabilities and security holes.
On its official site, ZF2 provides the documentation (tutorials and API reference), webinars,
community forums and commercial support services, like trainings and certification program.
ZF2 creators strive to improve the performance of ZF2 in comparison to the first version of the
framework. Among the features that contribute into the performance of ZF2, there are lazy class
autoloading and support of caching.
On the market, Zend Framework is not the only web development framework. ZF2 is positioned
as a good framework for corporate applications because of its pattern-based design and
scalability. However, you can use ZF2 in any-sized web application with great success.
Ive found a mistake in this chapter/have a suggestion. What do I do?
Please contact the author using the dedicated Forum. Alternatively, you can contact
the author through his E-mail address ([email protected]). The author appreciates your feedback and will be happy to answer you and improve this book.
https://leanpub.com/using-zend-framework-2/feedback
Unpack the downloaded ZIP archive to some directory. If you are programming in Linux, it is
recommended to unpack it in your home directory:
cp /path/to/downloaded/archive/ ZendSkeletonApplication-master.zip ~
cd ~
unzip ZendSkeletonApplication-master.zip
The commands above will copy the file ZendSkeletonApplication-master.zip archive that
youve downloaded to your home directory, then unpack the archive.
If you are using Windows, you can place the skeleton app directory anywhere in your
system, but ensure that file and directory access rights are sufficient for your web server
to read and write the directory and its files. Actually, if you dont put your files to
C:\Program Files, everything should be OK.
https://github.com/zendframework/ZendSkeletonApplication
23
24
Inside of the public directory, you can also find .htaccess file. Its main purpose is to define URL
rewriting rules, but you also can use it to define access rules for your web-site. For example, with
.htaccess you can grant access to your web-site from your own IP address only, or use HTTP
authorization to request users for username and password.
The public directory contains several subdirectories also publicly accessible by web-users:
css subdirectory contains all publicly accessible CSS files;
img subdirectory contains publicly accessible images (*.JPG, *.PNG, *.GIF, *.ICO, etc.);
25
and js subdirectory stores publicly accessible JavaScript files used by your web-pages.
Typically, files of jQuery library are placed here, but you can put your own JavaScript
files here, too.
Because the Zend Skeleton Application is stored on GitHub, inside of the directory structure,
you can find hidden .gitignore and .gitmodules files. These are GIT version control systems
files. You can ignore them (or even remove them if you do not plan to store your code in a GIT
repository).
Because we will later use the skeleton as the base for our Hello World application, lets rename
the ZendSkeletonApplication-master directory into helloworld, which also sounds shorter. In
Linux, you can do that with the following command:
mv ZendSkeletonApplication-master helloworld
http://jquery.com/
http://git-scm.com/
http://getcomposer.org/
https://packagist.org/
26
{
"name": "zendframework/skeleton-application",
"description": "Skeleton Application for ZF2",
"license": "BSD-3-Clause",
"keywords": [
"framework",
"zf2"
],
"homepage": "http://framework.zend.com/",
"require": {
"php": ">=5.3.3",
"zendframework/zendframework": ">2.2.0rc1"
}
}
What is JSON?
JSON (JavaScript Object Notation), is a text-based file format used for human-readable
representation of simple structures and nested associative arrays. Although JSON
originates from Java, it is used in PHP and in other languages, because it is convenient
for storing configuration data.
In that file, we see some basic info on the skeleton application (its name, description, license,
keywords and home page). You will typically change this info for your future web-sites. This
information is optional, so you can even safely remove it, if you do not plan to publish your web
application on Packagist catalog.
What is interesting for us now is the require key. The require key contains the dependencies
declarations for our application. We see that we require PHP engine version 5.3.3 or later and
Zend Framework 2.2.0 Release Candidate 1 or later.
The information contained in composer.json file is enough to locate the dependencies, download
and install them into the vendor subdirectory. Lets finally do that by typing the following
commands from your command shell (replace APP_DIR placeholder with your actual directory
name):
cd APP_DIR
php composer.phar self-update
php composer.phar install
The commands above will change your current working directory to APP_DIR, then self-update
the Composer to the latest available version, and then install your dependencies. By the way,
Composer does not install PHP for you, it just ensures PHP has an appropriate version, and if
not, it will warn you.
27
If you look inside the vendor subdirectory, you can see that it now contains a lot of files. Zend
Framework 2 files can be found inside the APP_DIR/vendor/zendframework/zendframework/library
directory (figure 2.3). Here you can encounter all the components that we described in Chapter
1 (Authentication, Barcode, etc.)
28
Currently, we have the skeleton application inside of home folder. To let Apache know about it,
we need to edit the virtual host file.
Virtual host file may be located at a different path, depending on your
operating system type. For example, in Linux Ubuntu it is located in
/etc/apache2/sites-available/000-default file. Moreover, virtual host file
name and content may look differently depending on Apache HTTP Servers version.
For OS- and server-specific information about virtual hosts, please refer to Appendix
A.
Lets now edit the default virtual host file to make it look like below (this example is applicable
to Apache v.2.2):
Virtual host file
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /home/username/helloworld/public
<Directory />
Options FollowSymLinks
AllowOverride None
</Directory>
<Directory /home/username/helloworld/public/>
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Order allow,deny
allow from all
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
LogLevel warn
</VirtualHost>
29
Line 1 of the file makes Apache to listen to all (*) IP addresses on port 80.
Line 2 defines the web masters E-mail address. If something bad happens to the site, Apache
sends an alert E-mail to this address. You can enter your E-mail here.
Line 4 defines the document root directory (APP_DIR/public). All files and directories under
the document root will be accessible by web-users. You should set this to be the absolute
path to skeleton applications public directory. So, the directories and files inside public (like
index.php, css, js, etc.) will be accessible, while directories and files above public directory
(like config, module, etc.) wont be accessible by web users, which enhances the security of the
web site.
Lines 5-8 define default access rules for directories. These rules are rather strict. The Options
FollowSymLinks directive allows Apache to follow symbolic links (in Linux, a symbolic links is
an analog of a shortcut in Windows). The AllowOverride None directive forbids overriding any
rules using .htaccess files.
Lines 9-14 define rules for the document root directory (APP_DIR/public). These rules override
the default rules mentioned above. For example, the AllowOverride All directive allows to
define any rules in .htaccess files. The Order allow,deny and allow from all control a threepass access control system, effectively allowing everyone to visit the site.
Line 16 defines the path to error.log file, which can be used to troubleshoot possible errors
occurring in your site code. Line 23 defines the logging level to use (the warn means that warnings
and errors will be written to log).
Lines 18-19 are comments and ignored by Apache. You mark comments with the hash (#)
character.
Zend Framework 2 utilizes Apaches URL rewriting module for redirecting web-users
to entry script of your web-site. Please ensure that your web-server has mod_rewrite
module enabled. For instructions on how to enable the module, please refer to Appendix
A.
After editing the config file, do not forget to restart Apache to apply your changes.
30
31
32
In the Run Configuration page, it is recommended that you specify the way you run the web site
(Local Web Site) and web site URL (http://localhost). Keep the Index File field empty (because
we are using mod_rewrite, the actual path to your index.php file is hidden by Apache). If you
are seeing the warning message like Index File must be specified in order to run or debug project
in command line, just ignore it.
Click the Finish button to create the project. When the helloworld project has been successfully
created, you should see the project window (see the figure 2.7).
In the project window, you can see the menu bar, the tool bar, the Projects pane where your
project files are listed, and, in the right part of the window, you can see the code of the index.php
entry file.
Please refer to Appendix B for more NetBeans usage tips, including launching and interactively
debugging ZF2-based web sites.
Its time for some advanced stuff
Congratulations! Weve done the hard work of installing and running the Zend Skeleton
Application, and now its time to have a rest and read about some advanced things in
the last part of this chapter.
33
The .htaccess (hypertext access) file is actually an Apache web servers configuration file
allowing to override some web servers global configuration. The .htaccess file is a directorylevel configuration, which means it affects only its owning directory and all sub-directories.
The content of .htaccess file is presented below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RewriteEngine On
# The following rule tells Apache that if the requested filename
# exists, simply serve it.
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
# The following rewrites all other queries to index.php. The
# condition ensures that if you are using Apache aliases to do
# mass virtual hosting, the base path will be prepended to
# allow proper resolution of the index.php file; it will work
# in non-aliased environments as well, providing a safe, one-size
# fits all solution.
RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$
RewriteRule ^(.*) - [E=BASE:%1]
RewriteRule ^(.*)$ %{ENV:BASE}index.php [NC,L]
34
Line 1 tells Apache web server to enable URL rewrite engine (mod_rewrite). The rewrite engine
modifies the incoming URL requests, based on regular expression rules. This allows you to map
arbitrary URLs onto your internal URL structure in any way you like.
Lines 4 - 7 define rewrite rules that tell the web server that if the client (web browser) requests
a file that exists in the document root directory, than to return the contents of that file as HTTP
response. Because we have our public directory inside of the virtual hosts document root, we
allow site users to see all files inside of the public directory, including index.php, CSS files,
JavaScript files and image files.
Lines 14 - 16 define rewrite rules that tell Apache what to do if the site user requests a file which
does not exist in document root. In such a case, the user should be redirected to index.php.
Table 2.2 contains several URL rewrite examples. The first and second URLs point to existing
files, so mod_rewrite returns the requested file paths. The URL in the third example points to
a non-existent file htpasswd (which may be a symptom of a hacker atack), and based on our
rewrite rules, the engine returns index.php file.
Table 2.2. URL rewrite examples
Requested URL
Rewritten URL
http://localhost/index.php
http://localhost/css/bootstrap.css
http://localhost/htpasswd
Order allow,deny
Deny from all
Allow from 127.0.0.1
Line 1 defines the order. Order allow deny means allow everything which is not denied.
Line 2 forces Apache to deny access to your site for everyone.
Line 3 overrides the line 2, allowing access to your site from IP 127.0.0.1 (localhost). You may
need to replace 127.0.0.1 with your external IP address.
35
AuthUserFile /usr/local/apache/passwd/passwords
AuthType Basic
AuthName "Authentication Required"
require valid-user
Line 1 defines the file where passwords will be stored. This file should be created with the
htpasswd utility.
Line 2 defines Basic authentication method. The most common method is Basic. It is important
to be aware, however, that Basic authentication sends the password from the client to the
server unencrypted. This method should therefore not be used for highly sensitive data. Apache
supports one other authentication method: AuthType Digest. This method is much more secure.
Most recent browsers support Digest authentication.
Line 3 defines the text that will be displayed to user when he tries to log in.
Line 4 will allow anyone to log in that is listed in the password file, and who correctly enters
their password.
To create passwords file, type the following command:
htpasswd -c /usr/local/apache/passwd/passwords <username>
In the command above, you should replace the <username> placeholder with the name of the
user. You can choose an arbitrary name, for example admin. The command will request the
users password and write the password to the file:
http://www.whatismyip.com/
36
When the user tries to visit the site, he sees the HTTP authentication dialog (see the figure below).
To log into your site, the visitor should enter the correct username and password.
For additional information on HTTP authentication, you can refer to Authentication
and Authorization topic of Apache documentation.
To access the web site, in your browsers navigation bar, enter http://localhost:8080.
After editing the virtual host config file, you should restart Apache to apply changes.
37
127.0.0.1
127.0.0.1:8080
site1.localhost
site2.localhost
So now youll be able to simply enter site1.localhost in your browsers address bar instead of
remembering the port number.
In Linux, the hosts file is located in /etc/hosts. In Windows, the file is typically
located in C:\Windows\System32\drivers\etc\hosts. To edit the file, you need to be
an administrator. Please also note that some anti-virus software may block changes
to hosts file, so youll have to temporarily disable your anti-virus to edit the file, and
enable it after.
38
Definition Example
Description
2.2.0
Exact version. In this example, only the version 2.2.0 can be installed.
>=2.2.0
>2.2.0
<=2.2.0
Lower or equal version can be installed (1.0, 1.5, 2.0.0, 2.2.0 etc.)
<2.2.0
!=2.2.0
>=2.0,<2.2.0
2.*
Any version having major number equal to 2 can be installed (minor number
can be any).
2.2
Any version starting from 2.2, but lower than the next major version
(equivalent to >=2.2,<3.00).
39
After installation, Composer also creates the APP_DIR/composer.lock file. This file now contains
actual versions of the packages that were installed. If you run the install command again,
Composer will encounter the composer.lock file, check which dependencies already installed
and as all packages already installed, it just exits without doing anything.
Now assume that in some period of time new security updates for your dependency packages
are released. You will want to update your packages to keep your web site secure. You can do
that by typing the following:
php composer.phar update
If you want to update only a single dependency, type its name as the following:
php composer.phar update zendframework/zendframework
After the update command, your composer.lock file will be updated, too.
What do I do if I want to roll back to a previous version of the package?
If the update procedure resulted in unwanted problems with your system, you can roll
back by reverting the changes to your composer.lock file and issuing the install
command again. Reverting changes to composer.lock is easy if you use a version
control system, like GIT or SVN. If you dont use a version control system, make a
backup copy of composer.lock before updating.
If you want to add new dependency to the application, you can either edit composer.json
manually, or issue require command. For example, to install Doctrine ORM module to your
web site (to add the doctrine/doctrine-module package to the application dependencies), type
the following:
php composer.phar require doctrine/doctrine-module 2.*
The command above edits composer.json file, and downloads and installs the package. We will
use this command later in Chapter 12, when becoming familiar with database management.
40
Definition Example
Description
php>=5.3.3
ext-dom, ext-pdo-mysql
lib-openssl
You can use composer show --platform command to display a list of available virtual packages
for your machine.
But there may be situations when you should store some dependent libraries under version
control:
If you have to make custom changes to third-party code. For example, assume you have
to fix a bug in a library, and you cannot wait for the librarys vendor to fix it for you (or
if the library vendor cannot fix the bug). In this case, you should place the library code
under version control to ensure your custom changes wont be lost.
If you have written a reusable module or library and want to store it in the vendor directory
without publishing it on Packagist.org. Because you dont have an ability to install this
code from the Packagist, you should store it under version control.
If you want a 100% guarantee that a third-party package wont be lost. Although the risk is
minimum, for some applications it is critical to be autonomous and not depend on package
availability on Packagist.org.
41
2.13 Summary
In this chapter, we have downloaded the Zend Skeleton Application project code from GitHub
code hosting and installed it using Composer dependency management tool. Weve configured
the Apache Virtual Host to tell the web server about location of the web sites document root
directory.
The skeleton application demonstrates the recommended directory structure of a typical web
site. We have the public directory containing files publicly accessible by site users, including
the index.php entry point file, CSS files, JavaScript files and images. All other directories of
the application are inaccessible by site users and contain application configuration, data and
modules.
In the second part of the chapter we discussed some advanced usage of hypertext access file
(.htaccess). With this file, you can protect your web site with password and allow accessing it
from certain IP addresses only.
The Composer dependency manager is a powerful tool for installing the dependencies of your
web site. For example, Zend Framework 2 itself can be considered as a dependency. All packages
installable by Composer are registered in a centralized catalog on the Packagist.org site.
Description
Zend\Mvc
Zend\Loader
Zend\ModuleManager
Zend\EventManager
Zend\ServiceManager
1
2
3
4
5
6
7
8
9
10
11
12
43
<?php
namespace Zend\Mvc;
// ...
/**
* Main application class for invoking applications.
*/
class Application
{
// ... class members were omitted for simplicity ...
}
You may notice that in example above we have the opening <?php tag which tells the
PHP engine that the text after the tag is a PHP code. In example above, when the file
contains only the PHP code (without mixing PHP and HTML tags), you dont need to
insert the closing ?> tag after the end of the code. Moreover, this is not recommended
and may cause undesired effects, if you occasionally add some character after the
closing ?> tag.
In Zend Framework 2, all classes belong to top-level Zend namespace. The line 2 defines
the namespace Mvc, which is nested into Zend namespace, and all classes of this component
(including the Application class shown in this example on lines 9-12) belong to this namespace.
You separate nested namespace names with the back-slash character (\).
In other parts of code, you reference the Application class using its full name:
<?php
$application = new \Zend\Mvc\Application;
It is also possible to use the alias (short name for the class) with the help of PHPs use statement:
<?php
// Define the alias in the beginning of the file.
use Zend\Mvc\Application;
// Later in your code, use the short class name.
$application = new Application;
44
Although the alias allows to use a short class name instead of the full name, its usage
is optional. You are not required to always use aliases, and can refer the class by its full
name.
Every PHP file of your application typically defines the namespace (except index.php entry
script and config files, which typically do not). For example, the main module of your site, the
Application module, defines its own namespace whose name equals to the module name:
<?php
namespace Application;
// ...
class Module
{
// ... class members were omitted for simplicity ...
}
45
As you can see from the example above, an interface is defined using the interface keyword,
almost the same way you define a standard PHP class. As a usual class, the interface defines
methods. However, the interface does not provide any implementation of its methods. In the
ApplicationInterface interface definition above, you can see that every application implementing this interface will have method getServiceManager() for retrieving the service manager
(about the service manager, see later in this chapter), the getRequest() and getResponse()
methods for retrieving the HTTP request and response, respectively, and method run() for
running the application.
In Zend Framework 2, by convention, interface classes should be named with
Interface suffix, like ApplicationInterface.
A class implementing an interface is called a concrete class. The concrete Application class
implements the ApplicationInterface, which means it provides the implementation of the
methods defined by the interface:
<?php
namespace Zend\Mvc;
//...
class Application implements ApplicationInterface
{
// Implement the interface's methods here
public function getServiceManager() {
//...
}
public function getRequest() {
//...
}
public function getResponse() {
//...
}
public function run() {
//...
}
}
The concrete Application class uses the implements keyword to show that it provides an
implementation of all methods of ApplicationInterface interface. The Application class can
also have additional methods, which are not part of the interface.
46
Graphically, the class relations are displayed using inheritance diagrams. In figure 3.1, the
diagram for Application class is presented. The arrow points from the child class to the parent
class.
As your application grows in size, it may be difficult to include each needed file. Zend Framework
2 itself consists of hundreds of files, and it can be very difficult to load the entire library and all
its dependencies this way. Moreover, when executing the resulting code, PHP interpreter will
take CPU time to process each included file, even if you dont create an instance of its class.
To fix this problem, in PHP 5.1, the class autoloading feature has been introduced. The PHP
function spl_autoload_register() allows you to register an autoloader function. For complex
web sites, you even can create several autoloader functions, which are chained in a stack.
During script execution, if PHP interpreter encounters a class name which has not been defined
yet, it calls all the registered autoloader functions in turn, until either the autoloader function
includes the class or not found error is raised. This allows for lazy loading, when PHP
interpreter processes the class definition only at the moment of class invocation, when it is really
needed.
To give you an idea of how an autoloader function looks like, below we provide a simplified
implementation of an autoloader function:
47
<?php
// Autoloader function.
function autoloadFunc($className) {
// Class map static array.
static $classMap = array(
'Zend\\Mvc\\Application' => '/path/to/zend/dir/Zend/Mvc/Application.php',
'Application\\Module' => '/path/to/app/dir/Application/Module.php',
//...
);
// Check if such a class name presents in the class map.
if(isset(static::$classMap[$className])) {
$fileName = static::$classMap[$className];
// Check if file exists and is readable.
if (is_readable($filename)) {
// Include the file.
require $filename;
}
}
}
// Register our autoloader function.
spl_autoload_register("autoloadFunc");
In the above example, we define the autoloadFunc() autoloader function, which we will further
refer to as the class map autoloader.
The class map autoloader uses the class map for mapping between class name and absolute path
to PHP file containing that class. The class map is just a usual PHP array containing keys and
values. To determine the file path by class name, the class map autoloader just needs to fetch
the value from the class map array. It is obvious, that the class map autoloader works very fast.
However, the disadvantage of it is that you have to maintain the class map and update it each
time you add a new class to your program.
48
Namespaces can have as many nesting levels as desired, but the Vendor Name should be
the top-level namespace.
Namespaces should map to directory structure. Each namespace separator (\) is converted
to a OS-specific DIRECTORY_SEPARATOR constant when loading from the file system.
The class name is suffixed with .php extension when loading the file from the file system.
For example, for the Zend\Mvc\Application class, you will have the following directory
structure:
/path/to/zend/lib
/Zend
/Mvc
Application.php
For the code conforming to the PSR-0 standard, we can write and register an autoloader, which
we will refer to as the standard autoloader:
<?php
// "Standard" autoloader function.
function standardAutoloadFunc($className) {
// Replace special characters in class name.
$className = str_replace('\\', '/', $className);
// Format the file path.
$fileName = "path/to/zend/dir/" . $className . ".php";
// Check if file exists and is readable.
if (is_readable($fileName)) {
// Include the file.
require $fileName;
}
}
// Register the autoloader function.
spl_autoload_register("standardAutoloadFunc");
The standard autoloader works as follows. Assuming that the class namespace can be mapped to
the directory structure one-by-one, the function calculates the path to PHP file by transforming
back-slashes (namespace separators) to forward slashes (path separators) and concatenating the
resulting path with the absolute path to the directory where the library is located. Then the
function checks if such a PHP file really exists, and if so, includes it with the require statement.
49
It is obvious, that the standard autoloader works slower than the class map autoloader. However,
its advantage is that you dont need to maintain any class map, which is very convenient when
you develop new code and add new classes to your application.
Zend Framework 2 conforms to PSR-0 standard, making it possible to use standard
autoloading mechanism across all its components. It is also compatible with other PSR0 conforming libraries like Doctrine or Symfony 2.
Thus, the main underlying goal of any web application is handling the HTTP request and
producing an HTTP response typically containing the HTML code of the requested web page.
The response is sent by the web server to the client web browser and the browser displays a web
page on the screen.
A typical HTTP request is presented below:
An HTTP request example
1
2
3
4
5
6
7
8
9
10
50
The starting line (line 1) specifies the method of the request (e.g GET or POST), the URL
string and HTTP protocol version.
Optional headers (lines 2-8) characterize the message, the transmission parameters and
provide other meta information. In the example above, each row represents a single header
in the form of name:value.
Optional message body contains message data. It is separated from the headers with a
blank line.
The headers and the message body may be absent, but the starting line is always present in the
request, because it indicates its type and URL.
The server response for the above request is presented below:
An HTTP response example
1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/html
Content-Encoding: gzip
Vary: Accept-Encoding
Server: Microsoft-IIS/7.5
Set-Cookie: ASPSESSIONIDQQRBACTR=FOCCINICEFAMEKODNKIBFOJP; path=/
X-Powered-By: ASP.NET
Date: Sun, 04 Aug 2013 13:33:59 GMT
Content-Length: 8434
(empty line)
(page content follows)
As you can see from the dump above, the HTTP response has almost the same format as the
request:
The starting line (line 1) represents the HTTP protocol version, response status code and
message (200 OK).
Optional headers (lines 2-10) provide various meta information about the response.
Optional message body follows the headers, and must be separated from headers by an
empty line. The message body typically contains the HTML code of the requested web
page.
51
Although the index.php file is very important, it is surprisingly small (see below):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
/**
* This makes our life easier when dealing with paths.
* Everything is relative to the application root now.
*/
chdir(dirname(__DIR__));
// Setup autoloading
require 'init_autoloader.php';
// Run the application!
Zend\Mvc\Application::init(
require 'config/application.config.php')->run();
Each application life stage is initiated by the application by triggering an event. Other classes
(either belonging to Zend Framework or specific to your application) may listen to events and
react accordingly.
Below, the four main events (life stages) are presented:
Bootstrap. When this event is triggered by the application, a module has a chance to register
itself as a listener of further application events in its onBootstrap() callback method.
52
Route. When this event is triggered, the requests URL is analyzed using a router class (typically,
with Zend\Mvc\Router\Http\TreeRouteStack class. If an exact match between the URL and a
route is found, the request is passed to the site-specific controller class assigned to the route.
Dispatch. The controller class dispatches the request using the corresponding action method
and produces the data that can be displayed on the web page.
Render. On this event, the data produced by the controllers action method are passed for
rendering to Zend\View\Renderer\PhpRenderer class. The renderer class uses a view template
file for producing an HTML page.
The event flow is illustrated in figure 3.2:
53
Some PHP frameworks prefer conventions over configuration concept, where most of
your parameters are hard-coded and do not require configuration. This makes it faster
to develop the application, but makes it less customizable. In Zend Framework 2, the
configuration over conventions concept is used, so you can customize any aspect of
your application, but have to spend some time for learning how to do that.
54
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
return array(
// This should be an array of module namespaces
// used in the application.
'modules' => array(
'Application',
),
// These are various options for the listeners
// attached to the ModuleManager
'module_listener_options' => array(
// This should be an array of paths in which
// modules reside. If a string key is provided,
// the listener will consider that a module
// namespace, the value of that key the specific
// path to that module's Module class.
'module_paths' => array(
'./module',
'./vendor',
),
// An array of paths from which to glob configuration
// files after modules are loaded. These effectively
// override configuration provided by modules themselves.
// Paths may use GLOB_BRACE notation.
'config_glob_paths' => array(
'config/autoload/{,*.}{global,local}.php',
),
// ...
);
In line 5 we have the modules key listing all modules which are present in your web site.
Currently, you have the single Application module.
In line 17, there is the module_paths key which tells ZF2 about directories where to look for
source files belonging to modules. Application modules that you develop are located under
APP_DIR/module directory, and third-party modules may be located inside the APP_DIR/vendor
directory.
And in line 26 we have the config_glob_paths key, which tells ZF2 where to look for extra
config files. You see that files from APP_DIR/config/autoload which have global.php or local.php
suffix, are automatically loaded.
Summing up, you typically use the main application.config.php file for storing the information
about which modules should be loaded into your app and where they are located and how they
55
are loaded (for example, you can control caching options here). In this file you can also tune the
service manager. It is not recommended to add more keys in this file. For that purpose it is better
to use autoload/global.php file.
56
module.config.php file
<?php
return array(
'router' => array(
'routes' => array(
// Register URL routing rules here.
),
),
'service_manager' => array(
// Register module-provided services here.
),
'controllers' => array(
// Register module-provided controllers here.
),
'view_helpers' => array(
// Register module-provided view helpers here.
),
'view_manager' => array(
// Provide view manager configuration here.
),
);
In this file, you register the modules controllers, put information about routing rules for mapping
URLs to your controllers, register controller plugins, and also register view templates and view
helpers (we will learn more about these terms in this chapter and in the next chapters).
57
Configuration files for each application module are loaded and merged. Modules are
loaded in the same order as they are listed in the application.config.php file. If two modules
store (either intentionally, or by mistake) parameters in the similar-named keys, these
parameters may be overwritten.
Extra config files from the APP_DIR/config/autoload folder are loaded and merged into
a single array. Then this array is merged with the module config array produced on the
previous stage, when loading the module configuration. Application-wide configuration
has higher priority than the module configuration, so you can override module keys here,
if you wish.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
namespace Application;
use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;
class Module
{
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
}
public function getConfig()
{
return include __DIR__ . '/config/module.config.php';
}
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
),
),
29
30
31
58
);
}
}
The class Module belongs to the modules namespace (for the main module it belongs to the
Application namespace).
The onBootstrap() method (lines 9-14) is called by the application on start up, during the
Bootstrap event, and allows to register event listeners for the module.
The getConfig() method (lines 16-19) tells the Zend Framework where the module.config.php
file is located.
The getAutoloaderConfig() method (lines 21-30) tells the Zend Framework where to find the
source files for the module. This information is used for initializing the autoloader function for
the module.
This makes it possible to automatically find and load any PHP class in any library installed with
Composer (including Zend Framework 2 classes).
59
The SplAutoloader interface defines the register() method, which is intended for registering
the autoloader function (class method in our case) with the help of spl_autoloader_register(),
and the autoload() method, which is intended for providing the concrete class discovery
algorithm.
60
In the above method, we see that the module tells ZF2 to use the Zend\Loader\StandardAutoloader
for autoloading the modules source files. The namespaces key contains keyvalue pairs, where
key is the namespace, and the value is the absolute path to the directory containing the PHP files
implementing the classes from that namespace. The __NAMESPACE__ constant expands into the
modules namespace string (for example, to Application for the main module of the application),
and the __DIR__ constant expands to directory path where the Module.php file is located.
61
For example, lets look at the IndexController.php file of Application module (figure 3.5).
IndexController class is the default controller for the skeleton web site. We will talk about controllers later in Chapter 4 when learning
about Model-View-Controller pattern.
62
<?php
namespace Application;
class Module {
//...
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\ClassMapAutoloader' => array(
__DIR__ . '/autoloader_classmap.php', // File-based class map.
array(
// Array class map.
'Application\Controller\IndexController' =>
__DIR__ . 'src/Application/Controller/IndexController.php,
//...
),
),
);
}
}
In the example above, we can provide the class map using an array (see lines 13-15) containing
keyvalue pairs, where the key is the full class name, and value is the absolute path to PHP
file where the class is implemented. Or we can create the class map in an external PHP file and
provide the autoloader the path to that file (line 12).
The problem with using the class map autoloader is that you have to maintain the class map,
which is difficult to do by hands. Fortunately, Zend Framework 2 provides you with the special
tool for this. The tool is a PHP script located in APP_DIR/vendor/bin/classmap_generator.php
file. To generate a class map file for your module, you call the generator script like below:
cd APP_DIR/vendor/bin/
php classmap_generator.php -l path/to/module -o /output/dir
63
for accessing the database, event manager service responsible for triggering events and delivering
them to event listeners, etc.
In Zend Framework 2, the ServiceManager class is a centralized repository for all application
services. The service manager is implemented in Zend\ServiceManager component, as the
ServiceManager class, which implements the ServiceLocatorInterface interface. The class
diagram is presented in figure 3.6:
The service manager is created on application start up (inside of init() static method of
Zend\Mvc\Application class). The standard services available through service manager are
presented in table 3.1. This table is incomplete, because the actual number of services registered
in service manager may be much bigger.
Service Name
Description
Application
ApplicationConfig
Config
EventManager
ModuleManager
Request
Response
Router
ServiceManager
ViewManager
A service is typically an arbitrary PHP class, but not always. For example, when ZF2 loads the
configuration files and merges the data into nested arrays, it saves the arrays in the service
64
manager as a couple of services (!): ApplicationConfig and Config. The first one is the array
loaded from application-level configuration file application.config.php, and the latter one is the
merged array from module-level config files and auto-loaded application-level config files. Thus,
in the service manager you can store any asset you want: a PHP class, a variable or an array.
From table 3.1, you can see that in ZF2 everything can be considered as a service. The service
manager is itself registered as a service. Moreover, the Application class is also registered as a
service.
An important thing you should note about the services is that they are typically stored
in a single instance only (this is also called the singleton pattern). Obviously, you dont
need the second instance of the Application class or, say, the event manager (in that
case you would have a nightmare with proliferating events).
In the web application, you typically deal with the ServiceLocatorInterface, instead of the
service manager itself.
You can test if a service is registered by passing its name to the service locators has() method.
It returns a boolean true if the service is registered, or false if the service with such a name is
not registered.
You can retrieve a service by its name at any place of your application with the help of the
service locators get() method. This method takes a single parameter representing the service
name. Look at the following example:
65
<?php
// Retrieve the application config array.
$appConfig = $serviceLocator->get('ApplicationConfig');
// Use it (for example, retrieve the module list).
$modules = $appConfig['modules'];
In the example above, we assume you have already retrieved the service locator from
somewhere. Looking ahead, lets say that typically, you will do that in your controller
classes with the help of getServiceLocator() method. We will learn about controller
classes later in Chapter 4.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
66
<?php
// Define a namespace where our custom service lives.
namespace Application\Service;
// Define a currency converter service class.
class CurrencyConverter {
// Converts euros to US dollars.
public function convertEURtoUSD($amount) {
return $amount*1.25;
}
//...
}
// Create an instance of the class.
$service = new CurrencyConverter();
// Save the instance to service manager.
$serviceManager->setService('CurrencyConverter', $service);
Above, in lines 5-13 we define an example CurrencyConverter class (for simplicity, we implement only a single method convertEURtoUSD() which is able to convert euros to US dollars).
Next, in line 17 we instantiate the class with the new operator, and in line 19 we register it with the
service manager using the setService() method (we assume that the $serviceManager variable
is of type Zend\ServiceManager\ServiceManager class, and that it was declared somewhere
else).
The setService() method takes two parameters: the service name string, and the service
instance. The service name should be unique within all other possible services. If you are trying
to register the service name which is already present, the setService() method throws an
exception. But sometimes you want to override the service with the same name (to replace it
by the new one). For this purpose, you can use the setAllowOverride() method of the service
manager:
1
2
3
4
5
6
<?php
// Allow to replace services
$serviceManager->setAllowOverride(true);
// Save the instance to service manager. There will be no exception
// even if there is another service with such a name.
$serviceManager->setService('CurrencyConverter', $service);
Above, the setAllowOverride() method takes the single boolean parameter defining whether
to allow you replace the service CurrencyConverter if such a name is already present, or not.
Once the service is stored in service manager, you can retrieve it by name at any place of your
application with the help of the service managers get() method. Look at the following example:
67
<?php
// Retrieve the currency converter service.
$service = $serviceManager->get('CurrencyConverter');
// Use it (convert money amount).
$convertedAmount = $service->convertEURtoUSD(50);
In the example above, we pass to the service manager the full class name of the service instead
of passing its instance. With this technique, the service will be instantiated by the service
manager only when someone calls the get('CurrencyConverter') method. This is also called
lazy loading.
As we see from the definition of the FactoryInterface, the factory class must provide the
createService() method returning the instance of a single service. The service locator is passed
to the createService() method as a parameter; it can be used during the construction of the
service for accessing other services.
As an example, lets write a factory for our currency converter service (see the code below). We
dont use complex construction logics for our CurrencyConverter service, but for more complex
services, you may need to use one.
68
<?php
use Zend\ServiceManager\FactoryInterface;
// Factory class
class CurrencyConverterFactory implements FactoryInterface{
public function createService(ServiceLocatorInterface $serviceLocator) {
// Create an instance of the class.
$service = new CurrencyConverter();
return $service;
}
}
Even more complex case of a factory is when you need to determine at run time which services
should be registered, and which should not. For such a situation, you can use an abstract factory.
An abstract factory class should implement the AbstractFactoryInterface interface:
<?php
namespace Zend\ServiceManager;
interface AbstractFactoryInterface
{
// Determine if we can create a service with such a name.
public function canCreateServiceWithName(
ServiceLocatorInterface $serviceLocator, $name, $requestedName);
// Create service with such a name.
public function createServiceWithName(
ServiceLocatorInterface $serviceLocator, $name, $requestedName);
}
69
<?php
// Register an alias for the CurrencyConverter service
$serviceManager->setAlias('CurConv', 'CurrencyConverter');
Once registered, you can retrieve the service by both its name and alias using the service locators
get() method.
<?php
return array(
//...
// Register the services under this key
'service_manager' => array(
'services' => array(
// Register service class instances here
//...
),
'invokables' => array(
// Register invokable classes here
//...
),
'factories' => array(
// Register factories here
//...
),
'abstract_factories' => array(
// Register abstract factories here
//...
),
'aliases' => array(
// Register service aliases here
//...
),
27
28
29
30
70
),
//...
);
In the example above, you can see that the service_manager key may contain several subkeys
for registering services in different ways:
the services subkey (line 7) allows to register class instances;
the invokables subkey (line 11) allows to register full class name of a service; the service
will be instantiated using lazy loading;
the factories subkey (line 15) allows for registering a factory, which is able to create
instances of a single service;
the abstract_factories (line 19) can be used for registering abstract factories, which are
able to register several services by name;
the aliases subkey (line 23) provides an ability to register an alias for a service.
As another example, lets look how this key looks like in the module.config.php file of the
Application module of the skeleton application:
<?php
return array(
//...
'service_manager' => array(
'abstract_factories' => array(
'Zend\Cache\Service\StorageCacheAbstractServiceFactory',
'Zend\Log\LoggerAbstractServiceFactory',
),
'aliases' => array(
'translator' => 'MvcTranslator',
),
),
);
Above, we can see that there are two abstract factories registered (for storage cache service and
logger service) and an alias is registered for the translator service. We will become familiar with
these services in the next chapters of this book.
3.12 Summary
In this chapter, weve learned some theory about ZF2-based web site operation basics.
ZF2 uses PHP namespaces and class autoloading features, simplifying the development of
applications which use many third-party components. The namespaces allow to solve the name
71
collisions between code components, and provide you with the ability to make the long names
shorter.
The class autoloading makes it possible to use any PHP class in any library installed with
Composer without the use of require_once statement.
Most of Zend Framework 2 components require configuration. You can define the configuration
parameters either at the application level, or at the module level.
The main goal of any web application is handling the HTTP request and producing an HTTP
response typically containing the HTML code of the requested web page. When Apache web
server receives an HTTP request from a client browser, it executes the index.php file, which is
also called the sites entry script. On every HTTP request, the Zend\Mvc\Application object is
created, whose life cycle consists of several stages (or events).
The web application can be also considered as a set of services. In Zend Framework 2, the service
manager is a centralized repository for all the application services. A service is typically a PHP
class, but in general it can be a variable or an array, if needed. In your code, you retrieve the
services from the service manager with the help of the service locator interface.
4. Model-View-Controller
In this chapter, you will learn about the models, views and controllers (MVC). The web
application uses the MVC pattern to separate business logic from presentation. The goal of this
is to allow for code reusability and separation of concerns.
ZF2 components covered in this chapter:
Component
Description
Zend\Mvc
Support of MVC pattern. Implements base controller classes, controller plugins, etc.
Zend\View
Implements the functionality for variable containers, rendering a web page and
common view helpers.
Zend\Http
Zend\Version
A small auxiliary component, which can be used for checking the version of Zend
Framework.
The Hello World is a complete web site which can be installed on your machine. To install the
example, you can either edit your default Apache virtual host file or create a new one. After
editing the file, restart the Apache HTTP Server and open the web site in your web browser.
https://github.com/olegkrivtsov/using-zend-framework-2-book
Model-View-Controller
73
Figure 4.1. The Hello World sample can be downloaded from GitHub
Since that time, PHP became object-oriented, and now you can organize your code into classes.
The Model-View-Controller (MVC) pattern is just a set of advices telling you how to organize
your classes in a better manner, to make them easy to maintain.
In MVC, classes implementing your business logic are called models, code snippets rendering
HTML pages are called views, and the classes responsible for interacting with user are called
controllers.
Views are implemented as code snippets, not as classes. This is because views are
typically very simple and contain only the mixture of HTML and inline PHP code.
Model-View-Controller
74
The main objective of the MVC concept is to separate the business logic (models) from its
visualization (views). This is also called the separation of concerns, when each layer does its
specific tasks only.
By separating your models from views, you reduce the number of dependencies between them.
Therefore, changes made to one of the layers have the lowest possible impact on other layers.
This separation also improves the code reusability. For example, you can create multiple visual
representations for the same models.
To better understand how this works, lets remember that any web site is just a PHP program
receiving an HTTP request from the web server, and producing an HTTP response. Figure 4.2
shows how an HTTP request is processed by the MVC application and how the response is
generated:
First, the site visitor enters an URL in his web browser, for example http://localhost, and
the web browser sends the request to the web server over the Internet.
Web servers PHP engine runs the index.php entry script. The only thing the entry script
does is creating the Zend\Mvc\Application class instance.
The application uses its router component for parsing the URL and determining to which
controller to pass the request. If the route match is found, the controller is instantiated and
its appropriate action method is called.
In the controllers action method, parameters are retrieved from GET and POST variables.
To process the incoming data, the controller instantiates appropriate model classes and
calls their methods.
Model classes use business logic algorithms to process the input data and return the
output data. The business logic algorithms are application-specific, and typically include
retrieving data from database, managing files, interacting with external systems and so
on.
The result of calling the models are passed to the corresponding view script for the
rendering of the HTML page.
View script uses the model-provided data for rendering the HTML page.
Controller passes the resulting HTTP response to application.
Web server returns the resulting HTML web page to the users web browser.
The user sees the page in browser window.
Now you might have some idea how models, views and controllers cooperate to generate HTML
output. In the next sections, we describe them in more details.
Model-View-Controller
75
4.3 Controllers
A controller provides communication between the application, models and views: gets input
from HTTP request and uses the model(s) and the corresponding view to produce the necessary
HTTP response.
Controllers belonging to module typically reside in the Controller subdirectory of modules
source directory (shown in figure 4.3).
76
Model-View-Controller
Zend Skeleton Application provides you with the default implementation of IndexController
class. The IndexController is typically the main controller class of the web site. Its code is
presented below (some parts of code were omitted for simplicity):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
// IndexController.php
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class IndexController extends AbstractActionController {
// The "index" action
public function indexAction() {
return new ViewModel();
}
}
From the example above, you can see that controllers usually define their own namespace
(line 3). The Index controller, as all other controllers from the Application module, lives in
Application\Controller namespace.
A controller is a usual PHP class derived from the AbstractActionController base class (line
8).
By default, the controller class contains the single action method called indexAction() (see lines
11-14). Typically, you will create other action methods in your controller classes.
ZF2 automatically recognizes the action methods by the Action suffix. If a controller
methods name does not have that suffix, it is considered as a usual method, not an
action.
77
Model-View-Controller
As its name assumes, an action method performs some site action, which typically results in
displaying a single web page. Index controller usually contains action methods for site-wide
web pages (table 4.1). For example, you would have index action for the Home page, about
action for About page, contactUs action for the Contact Us page and possibly other actions.
Table 4.1. Index controllers typical actions
Action Method
Description
IndexController::indexAction()
IndexController::aboutAction()
IndexController::contactUsAction()
The AbstractActionController provides you with several useful methods you can use in your
controller classes. Table 4.2 provides you with a brief summary of the methods:
Table 4.2. AbstractActionControllers useful methods
Method Name
Description
getRequest()
getResponse()
getServiceLocator()
78
Model-View-Controller
Method Name
Description
getEventManager()
getEvent()
getPluginManager()
plugin($name, $options)
__call($method, $params)
As you can see from the table above, the base controller class provides you with access to HTTP
request and response data, and provides you with the access to the service manager and to the
event manager. It also gives you an ability to register and call controller plugins (we will learn
about controller plugins later in this chapter).
The code above returns the instance of Zend\Http\Request class, containing all the HTTP
request data. In table 4.3, you can find the most widely used methods of the Request class together
with their brief description.
Method Name
Description
isGet()
isPost()
isXmlHttpRequest()
isFlashRequest()
getMethod()
getUriString()
79
Model-View-Controller
Method Name
Description
getQuery($name, $default)
getPost($name, $default)
getCookie()
getFiles($name, $default)
getHeaders($name, $default)
getHeader($name, $default)
renderRequestLine()
fromString($string)
toString()
In the example above, we used the Params controller plugin, which provides you with convenient
methods of accessing GET and POST variables, uploaded files, etc.
In line 2 we use the fromQuery() method for retrieving a variable having name var_name from
GET. If such a variable does not present, the default value default_val is returned. The default
value is very convenient, because you dont have to use the isset() PHP function to test if the
variable exists.
In line 5 we use the fromPost() method to retrieve the variable from POST (line 5). The meaning
of this methods parameters is the same as for the fromQuery() method.
In ZF2, you must not access request parameters through traditional PHP $_GET and
$_POST global arrays. Instead, you use ZF2-provided API for retrieving the request
data.
80
Model-View-Controller
Method Name
Description
fromString($string)
getCookie()
setStatusCode($code)
getStatusCode()
setReasonPhrase($reasonPhrase)
getReasonPhrase()
getBody()
isForbidden()
isNotFound()
isOk()
isServerError()
isRedirect()
isSuccess()
renderStatusLine()
toString()
Model-View-Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
81
The Zend\Version component is a small auxiliary component, which can be used by you if you
need to check the version of Zend Framework. To do that, you use the Version class.
In line 9, we use the VERSION constant defined in the Version class, which is the literal
representation of the version of Zend Framework currently installed in our machine (at the
moment of writing this text, this constant is equal to 2.2.1).
In line 11, we use the getLatest() static method to load the latest available version of ZF from
the Internet (note, that this requires the Internet connection and may take a few seconds to
complete).
In line 13, we use the compareVersion() static method to compare our current version with the
latest one (the comparison result will be -1 if our current version is newer, 0 if both equal, or 1 if
ours is older).
In lines 17-21, we pass the variables weve created to the constructor of the ViewModel object as
an associative array. The array keys define the names of the variables which on return will be
accessible to view script.
The ViewModel class provides several methods that you can additionally use to set variables to
ViewModel and and retrieve variables from it. The table 4.5 provides the methods summary:
82
Model-View-Controller
Method name
Description
getVariable($name, $default)
setVariable($name, $value)
Sets a variable.
setVariables($variables, $overwrite)
getVariables()
clearVariables()
<?php
return array(
// ...
'controllers' => array(
'invokables' => array(
'Application\Controller\Index' =>
'Application\Controller\IndexController'
// Put other controllers registration here
),
),
// ...
);
In line 5, we have the the controllers key, which contains the invokables subkey. You should
register your controllers here. To register a controller class, you add the line in form of keyvalue
pair. The key should be the unique ID of the controller, like Application\Controller\Index, and
value should be the fully qualified class name of the controller, like Application\Controller\IndexController.
By registering your controller under the invokables subkey, you tell Zend Framework
that it can invoke the controller by instantiating it with the new operator. This is the
most simple way of instantiating the controller. As an alternative, you can register a
factory to create the controller instance, in that case you would register your controller
under the factories subkey.
83
Model-View-Controller
There are several standard controller plugins available out of the box (table 4.6), and weve
already used one of them (the Params plugin) in one of our previous examples.
Table 4.6. Standard Controller Plugins
Description
Params
Url
Layout
Identity
Returns the identity of the user who has logged into the
web site.
Composition is a relationship between two classes that is best described as a has-a and whole/part relationship. The owner class contains
a reference to another class (plugin). The owner is responsible for the lifetime of the object it holds.
84
Model-View-Controller
Description
FlashMessenger
Redirect
PostRedirectGet
FilePostRedirectGet
Inside of the controllers action method, you access a plugin in the following way:
1
2
3
4
5
6
7
8
Model-View-Controller
85
To let Zend Framework 2 know about your plugin, you need to register it in your module.config.php file under the controller_plugins key. See below for example:
<?php
return array(
// ...
'controller_plugins' => array(
'invokables' => array(
'Access' => 'Application\Controller\Plugin\AccessPlugin',
)
),
// ...
);
After that, youll be able to access your custom plugin from all of your controllers actions in
this way:
// Check if site user is allowed to visit the "index" page
$isAllowed = $this->access()->checkAccess('index');
4.8 Views
Views belong to the presentation layer of the web application, because their goal is to produce
HTML output returned by the web server to site visitors.
In Zend Framework 2, you implement a view as a template file, which is a file having .phtml
extension (phtml stands for PHP+HTML). View templates have such a name because they
usually contain HTML code mixed with PHP code snippets used for rendering the web pages.
Views typically live inside of the view subdirectory of the module (see figure 4.5):
86
Model-View-Controller
Why are view template files not stored under modules source directory?
View templates (.phtml files) are not stored under modules src/ directory, because
they are not usual PHP classes and do not need to be resolved by a PHP class
autoloading feature. View templates are resolved by the special ZF2 class called view
resolver, and for this reason, view templates are stored under the modules view
directory.
View templates can have different behaviors, based on variables you pass to them from the
controllers action method. Data are passed to view templates with the help of a ViewModel
variable container.
For example, lets implement the view template for the aboutAction() of our Index controller.
The About page will display the title, some information about our Hello World application, and
the information about the current Zend Framework version.
To create the view template file, in your NetBeans window, navigate to view/application/index
directory (see figure 4.6), and right click on the index directory name. From the context menu
that appears, select the New->PHP File menu item.
In the New PHP File dialog that appears (figure 4.7), enter the name about.phtml and click the
Finish button.
The about.phtml view template file will be created and displayed in the right pane of NetBeans
window. In that file, enter the following:
87
Model-View-Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<h1>About</h1>
<p>
The Hello World application.
</p>
<p>
Your Zend Framework version is
<?php echo $this->zendFrameworkVer; ?>
</p>
<?php if($this->isNewerVerAvailable): ?>
<p>
Your Zend Framework version is outdated. The latest available
version is <?php echo $this->latestVer; ?>.
</p>
<?php endif; ?>
As you can see, the view template is a usual HTML page with several PHP code fragments. A view
script just renders the data you pass to it with a ViewModel variable container. For example, in
88
Model-View-Controller
line 9 we get the value of $zendFrameworkVer variable and print it with the echo PHP statement.
In your view script, you can also use simple flow control operations (like if, foreach or switch)
to make the appearance of the page different depending on variables value. For example, in line
12 we have the if statement testing if the new Zend Framework version is available, and if yes,
the Your Zend Framework version is outdated text paragraph is displayed.
Now lets look at how the page looks like in the web browser. Type http://localhost/application/index/about
URL in your browsers navigation bar. The About page should appear (see figure 4.8):
89
Model-View-Controller
In general, the PHP code you use inside of views must be as simple as possible. Views
typically do not modify the data you pass from controller. For example, a view can use
the model you pass to it to walk through database table rows and render the items to
an HTML page, but it should never create database tables or modify them itself.
ZF2 provides many standard view helpers out of the box. In the table 4.7, some of them are
presented with a brief description:
90
Model-View-Controller
Description
BasePath
Url
ServerUrl
Doctype
PageTitle
HtmlList
ViewModel
Layout
Partial
InlineScript
Identity
FlashMessenger
To demonstrate the usage of a view helper, below we will show how to set a title for a web
page. Typically, it is required to give a different title per each web page. You can do this with
the HeadTitle view helper. For example, you can set the title for the About page by adding the
following PHP code in the beginning of the about.phtml view template:
<?php
$this->headTitle('About');
?>
In the code above, we call the HeadTitle view helper and pass it the page title string (About) as
the argument. The HeadTitle view helper internally sets the text for the <title> HTML element
of your web page. Then, if you open the About page in your web browser, the page title will look
like About - Zend Skeleton Application (see the figure below for an example):
91
Model-View-Controller
We will discuss the view helpers in more details and provide more usage examples in
Chapter 6.
Method name
Description
setTemplate()
getTemplate()
To set the view template name, you use the setTemplate() method. The getTemplate() method
returns the view template name currently set for the view model.
Model-View-Controller
92
The following code example shows how you can call the setTemplate() method from your
IndexController class indexAction() method to force ZF2 to use the about.phtml view
template file for rendering the Home page, instead of the index.phtml file:
1
2
3
4
5
6
7
8
In the code above, we created a new instance of the ViewModel class as usual (line 5).
Then we called the setTemplate() method on the view model object (line 6) and passed the
name of the view template name as its argument. The view template name is actually a relative
path to the about.phtml file, minus file extension.
Finally, we returned the view model object from the action method (line 7).
However, calling the setTemplate() method in every action method is optional. If you dont
do that, ZF2 will determine the view template name automatically by concatenating the current
module name, controller name and action method name.
When Zend Framework has the template name, it only remains to determine the absolute path
to the corresponding .phtml file. This is also called the view template resolving. View templates
are resolved with the special Zend Frameworks class called the view resolver.
In ZF2, there are two view resolvers out of the box: TemplatePathStack and TemplateMapResolver.
Both resolvers take a view template name as input, and return path to view template file as
output. The template name is composed of controller name followed by template name, like
application/index/about, application/index/index, layout/layout and so on.
The template map resolver uses a PHP nested array to determine path to view template
file by its name. This way is fast, but you have to maintain some template map array and
update it each time you add a new view script.
The template path stack resolver assumes that the view template name can be mapped
to directory structure. For example, index/about template name maps to APP_DIR/module/Application/view/application/index/about.phtml. This way is simpler, because you
dont have to maintain any maps.
View resolver settings are stored inside of your module.php.config file under the view_manager
key:
Model-View-Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
93
<?php
return array(
//...
'view_manager' => array(
//...
'template_map' => array(
'layout/layout' =>
__DIR__ . '/../view/layout/layout.phtml',
'application/index/index' =>
__DIR__ . '/../view/application/index/index.phtml',
'error/404' => __DIR__ . '/../view/error/404.phtml',
'error/index'=> __DIR__ . '/../view/error/index.phtml',
),
'template_path_stack' => array(
__DIR__ . '/../view',
),
),
);
You can see that template map resolvers settings are stored under the template_map key. By
default, there are several standard view templates, which are resolved this way: the index page
template, the layout template (we will talk about it in Chapter 6) and error templates (we will talk
about them a little bit later). These standard pages are served with this type of resolver, because
it is fast.
The template path stack resolvers settings are stored under the template_path_stack key. You
can see that this resolver looks for your view scripts under the view directory of your module.
Thats why we could just put about.phtml file under that directory, and ZF will automatically
find the template.
The template map resolver and template path stack resolver work in pair. First, the fast template
map resolver tries to find the template view in its array map, and if the page is not found, the
template path stack resolver is executed.
Model-View-Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
/**
* This is the controller class for managing file downloads.
*/
class DownloadController extends AbstractActionController {
/**
* This is the 'file' action that is invoked
* when a user wants to download the given file.
*/
public function fileAction()
{
// Get the file name from GET variable
$fileName = $this->params()->fromQuery('name', '');
// Take some precautions to make file name secure
str_replace("/", "", $fileName); // Remove slashes
str_replace("\\", "", $fileName); // Remove back-slashes
// Try to open file
$path = './data/download/' . $fileName;
if (!is_readable($path)) {
// Set 404 Not Found status code
$this->getResponse()->setStatusCode(404);
return;
}
// Get file size in bytes
$fileSize = filesize($path);
// Write HTTP headers
$response = $this->getResponse();
$headers = $response->getHeaders();
$headers->addHeaderLine(
"Content-type: application/octet-stream");
$headers->addHeaderLine(
"Content-Disposition: attachment; filename=\"" .
$fileName . "\"");
$headers->addHeaderLine("Content-length: $fileSize");
$headers->addHeaderLine("Cache-control: private");
94
Model-View-Controller
47
48
49
50
51
52
53
54
55
56
57
58
59
60
95
The action method takes the name parameter from URLs query part (line 19), removes slashes
from file name (lines 22-23), adds HTTP headers to Response object (lines 39-45) and file contents
(lines 48-55). Finally, it returns the Response object to disable the default view rendering.
Register the DownloadController class by adding the following line to your module.config.php
file:
<?php
return array(
// ...
'controllers' => array(
'invokables' => array(
// ...
'Application\Controller\Download' =>
'Application\Controller\DownloadController'
),
),
// ...
);
To see how the file download works, create APP_DIR/data/download directory and put some text
file named sample.txt in it. Then open your web browser and type the URL http://localhost/application/downloa
in your browsers navigation bar and press the Enter key. The browser will download the
sample.txt file and offer you to save it to some location.
96
Model-View-Controller
error (shown in figure 4.10), and error/index which is displayed when an unhandled exception
is thrown somewhere inside of the application.
The module.config.php file contains several parameters under the view_manager key, which you
can use to configure the appearance of your error templates:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
return array(
//...
'view_manager' => array(
'display_not_found_reason' => true,
'display_exceptions'
=> true,
//...
'not_found_template'
=> 'error/404',
'exception_template'
=> 'error/index',
'template_map' => array(
//...
'error/404' => __DIR__ . '/../view/error/404.phtml',
'error/index'=> __DIR__ . '/../view/error/index.phtml',
),
'template_path_stack' => array(
__DIR__ . '/../view',
Model-View-Controller
18
19
20
97
),
),
);
The display_not_found_reason parameter controls whether to display the detailed information about the Page not Found error.
The display_exceptions parameter defines whether to display information about an
unhandled exception and its stack trace.
The not_found_template defines the template name for the 404 error.
The exception_template specifies the template name for the unhandled exception error.
4.13 Models
A model is a PHP class which contains the business logic of your application. The business logic
is the core of your web site which implements the goal of site operation. For example, if you
implement an E-shop web site, you will have models implementing the product catalog and the
shopping cart.
In general, the term model means a simplified representation of a real-life object or phenomenon.
Simplified because the real-life object has infinite amount of properties. For example, a reallife person who visits your site consists of billions of atoms, and you cannot describe them all.
Instead, you take several properties of the object, which are the most important for your system
and ignore all others. For example, the most important properties of the site visitor (from web
site architects point of view) are first name, last name, country, city, ZIP code and street address.
Models can have some behavior. For example, a mailer model may send E-mail messages, the
currency converter model may be able to convert money and so on.
With ZF2, you represent models as usual PHP classes. Properties are implemented as class fields,
and the behaviors are implemented as class methods.
98
Model-View-Controller
Model Type
Directory
Entities
APP_DIR/module/Application/src/Application/Entity
Value Objects
APP_DIR/module/Application/src/Application/ValueObject
Services
APP_DIR/module/Application/src/Application/Service
Repositories
APP_DIR/module/Application/src/Application/Repository
Factories
APP_DIR/module/Application/src/Application/Factory
Separation of models into different types make it easier to design your business logic
domain. This is also called the Domain Driven Design (or shortly, DDD). The person
who proposed DDD was Eric Evans in his famous book called Domain-Driven Design
Tackling Complexity in the Heart of Software.
4.14.1 Entities
Entities always have some identifier property, so you can uniquely identify the object. For
example, a User entity always has a unique login property, and you can identify the user by
that attribute. You can change some other attributes of the entity, like firstName, or address,
but its identifier never changes. Entity models are usually stored in a database, in a file system
or in any other storage.
Below, you can find an example a User entity, which represents a site visitor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//
//
//
//
//
//
//
//
e.g.
e.g.
e.g.
e.g.
e.g.
e.g.
e.g.
e.g.
// Behaviors
public getLogin() {
return $this->login;
}
public setLogin($login) {
"admin"
"Mr."
"John"
"Doe"
"USA"
"Paris"
"10543"
"Jackson rd."
Model-View-Controller
20
21
22
23
24
99
$this->login = $login;
}
//...
}
In lines 5-10, we define User models properties. The best practice is to define the properties using
the private access type, and make them available to the caller through getter and setter public
methods (like getLogin() and setLogin(), etc).
Models behavior methods are not limited by getters and setters. You can create
other methods which manipulate with models data. For example, you can define the
getFullName() convenience method, which would return the users full name, like Mr.
John Doe.
class MoneyAmount {
// Properties
private $currency;
private $amount;
// Constructor
public function __construct($amount, $currency='USD') {
$this->amount = $amount;
$this->currency = $currency;
}
// Gets the currency code
public function getCurrency() {
return $this->currency;
}
// Gets the money amount
public function getAmount() {
return $this->amount;
}
Model-View-Controller
22
23
24
25
26
27
28
29
30
31
32
33
34
100
In lines 4-5 we define two properties: currency and amount. The model has no identifier property,
instead it is identified by all properties as a whole: if you change either the currency or amount,
you would have a different money amount object.
In lines 8-10 we define the constructor method, which initializes the properties. In lines 14-21,
we define getter methods for models properties. Note that we do not have setter methods (the
model is immutable).
In lines 24-33 we have the static method convert(), which is designed to convert money amount
to another currency using some simple currency conversion algorithm. Instead of modifying the
money amount properties, it constructs and returns a new MoneyAmount object.
The real life is much more difficult than we described above, and you should be
flexible enough to adapt your entities and value objects to real conditions. In some
situations you will have a value object which is mutable (have setter methods), and
sometimes, you will have only getter methods for some of entity properties. Do not
dwell on the idealistic recommendations provided in this chapter, instead adapt your
models as your intuition advices to you.
4.14.3 Services
Service models usually encapsulate some business logic functionality. Services typically do not
have state (internal properties), instead they manipulate other entity models and value objects.
Services usually have easily recognizable names ending with er suffix, like FileUploader or
UserManager.
Below, an example of Mailer service is presented. It has the sendMail() method which takes an
EmailMessage value object and sends an E-mail message using standard PHP mail() function:
Model-View-Controller
101
Model-View-Controller
102
4.14.4 Factories
Factories are usually being designed to instantiate other models. In the simplest cases you
can create an instance of a class without any factory, just by using the new operator, but
sometimes class creation logic might be very complex, and you encapsulate it inside of a factory
class. Factory classes typically have names ending with Factory suffix, like ResourceFactory,
FileFactory, etc.
For example, lets consider the case when you use a CAPTCHA image in a web form. The image
is a type of challenge-response test used to determine whether the user is a human or a robot.
There may be different CAPTCHA types: simple image captcha, reCAPTCHA and so on. Lets
create a fictitious CaptchaFactory class, whose only goal is to create different CAPTCHA images
based on the $type argument you pass to its createCaptcha() method.
// The CaptchaFactory class is used to create CAPTCHA
// image for a web form.
class CaptchaFactory {
// Creates a CAPTCHA object based on type parameter
public static function createCaptcha($type) {
if($type=='Image')
return new ImageCaptcha();
else if($type=='ReCaptcha')
return new ReCaptcha();
else
throw new Exception('Unknown captcha type');
}
}
4.14.5 Repositories
Repositories are specific models responsible for storing and retrieving entities. For example, a
UserRepository may represent a database table and provide methods for retrieving User entities.
You typically use repositories when storing entities in a database. With repositories, you can
encapsulate SQL query logic in the single place and easily maintain and test it.
We will learn about repositories in more details in Chapter 12, when talking about
Doctrine library.
http://www.google.com/recaptcha
Model-View-Controller
103
When writing your own application having specific model domain, you may be confused when
trying to decide to which model type your class belongs (whether it is an entity, value object,
repository, service or factory). Below, a simple algorithm is provided to make it easier for you to
determine the correct model type when writing your own application:
Your model class is definitely a Service
if you call it from your controller class
if it has no state (private attributes)
if you think the best name for it ends with er: suffix, like FileUploader or
VersionChecker
Model-View-Controller
104
accesses user request data ($_GET, $_POST, $_FILES and other PHP variables);
(optionally) makes some basic preparations to the data;
instatiates the model class(es) (or gets service(s) registered in the ServiceManager);
passes the data to model(s) and retrieves the result returned by the model(s);
and finally returns the output data as a part of a ViewModel variable container.
Model-View-Controller
14
15
16
17
18
19
20
21
22
105
Model-View-Controller
106
If you follow this principles, you will encounter that your models are easy to test, because they
have clearly identified input and output. You can write a unit test which passes some test data
to input end of the model, retrieves the output data and verifies that the data is correct.
If you are confused whether to put certain code in a controller or in a model, ask yourself: is this
an important logic that needs to be carefully tested? If the answer is yes, you should put the code
in a model.
4.17 Summary
A Zend Framework 2 based web site is just a PHP program receiving an HTTP request from
the web server, and producing an HTTP response. The web application uses the Model-ViewController pattern to separate business logic from presentation. The goal of this is to allow for
code reusability and separation of concerns.
A controller is a mediator between the application, models and views: it gets input from HTTP
request and uses the model(s) and the corresponding view to produce the necessary HTTP
response. A controller is a usual PHP class containing action methods.
Views are simple HTML+PHP code snippets producing HTML output returned by the web server
to site visitors. You pass the data to view scripts through the ViewModel variable container.
A model is a PHP class which contains the business logic of your application. The business logic
is the core of your web site which implement the goal of site operation. Models can access
database, manipulate disk files, connect to external systems, manipulate other models and so on.
5. URL Routing
When a site user enters a URL in a web browser, the request is finally dispatched to controllers
action. In this chapter, we will learn about how ZF2-based application maps page URLs to
controllers and their actions. This mapping is accomplished with the help of routing. Routing
is implemented as a part of Zend\Mvc component.
ZF2 components covered in this chapter:
Component
Description
Zend\Mvc
Zend\Barcode
This URL begins with a scheme segment (the scheme typically looks like http or https).
Then, the host name segment follows which is the domain name of your web server (like
site1.yourserver.com). Optional path segments follow the host name. So if you have the path
part /path/to/page then path, to, and page would each be a URL segment. Next, after
the question mark, the optional query part follows. It consists of one or several name=value
parameters separated from each other by an ampersand character (&).
Each segment in a URL uses special character encoding, which is named the URL encoding. This
encoding ensures that the URL contains only safe characters from the ASCII table. If a URL
contains unsafe characters, they are replaced with a percentage character (%) followed by two
hexadecimal digits (for example, the space character will be replaced by %20).
ASCII (American Standard Code for Information Interchange) is a character set which can be used to encode characters from the English
alphabet. It encodes 128 characters: digits, letters, punctuation marks and several control codes inherited from Teletype machines.
108
URL Routing
Route Type
Description
Literal
Segment
Regex
Wildcard
Hostname
Scheme
Method
Matching an HTTP method (e.g. GET, POST, etc.) against some criteria.
Each route type in the table above (except the Method type) may be matched against a specific
segment (or several segments) of a URL. The Method route type is matched against the HTTP
method (either GET or POST) retrieved from HTTP request.
There is also the Query route type, which is declared deprecated and is not recommended to use. This route type is actually not needed, because you can retrieve query
parameters from your URL with the Params controller plugin (see the Retrieving Data
from HTTP Request section in Chapter 4).
109
URL Routing
Route Type
Description
SimpleRouteStack
TreeRouteStack
Part
Chain
The TreeRouteStack and SimpleRouteStack are used as the top-level route types. The
SimpleRouteStack allows to organize different routing rules in a priority list. The TreeRouteStack
allows to nest different routing rules, forming a tree.
Figure 5.2 shows the route class inheritance diagram.
As you can see from the image, all route classes are inherited from RouteInterface interface (we
will learn this interface in details in the Writing Own Route Type section later in this chapter).
The SimpleRouteStack is a parent class for TreeRouteStack class, which inherits the behavior
of the simple route stack (allows to organize routes in priority list) and extends it (allows to
organize routes in subtrees). The Part and Chain classes are derived from TreeRouteStack class
and are used internally by the TreeRouteStack for building subtrees and chains of child routes.
110
URL Routing
You may notice that the SimpleRouteStack class lives in the Zend\Mvc\Router namespace, while other route classes live in its sub-namespace Zend\Mvc\Router\Http. This
is because routing is also used for mapping shell commands to controllers in console
applications. Thus, console route classes will live in Zend\Mvc\Router\Console, while
the SimpleRouteStack compound route type will be used for both HTTP routing and
console routing.
Figure 5.3. An example of Simple Route Stack (left) and Tree Route Stack (right)
111
URL Routing
a branch containing a single Segment route, and a branch consisting of Scheme, Hostname and
Segment routes.
The tree route stack performs request matching in the following way. It walks through its priority
list items (denoted by dashed lines in figure 5.3), starting from high-priority routes. If a certain
item is a Chain route or a Part route, it processes such a nested route from its parent route to
children. If the parent route matches, the children (denoted with solid lines) are analyzed then.
The nested route is considered matching if at least one route matches in each tree (or chain) level.
Each route in a tree (or chain) consumes a part of the URL (figure 5.4). The parent route is
matched against the first segment (or several segments) of the URL, its child is matched again
the next segment (or several segments), and so on, until the end of the URL string is reached.
<?php
return array(
//...
'router' => array(
'router_class' => 'Zend\Mvc\Router\Http\TreeRouteStack',
'routes' => array(
// Register your routing rules here...
),
'default_params' => array(
// Specify default parameters here for all routes here ...
)
112
URL Routing
12
13
),
);
Above, in line 4 we have the router key, under which there is the routes subkey (line 6), containing
the routing rules.
You can specify which top-level route class to use (either TreeRouteStack or SimpleRouteStack)
with the router_class parameter (line 5). If this parameter is not present, the TreeRouteStack
is used by default.
You can use the optional default_params key (line 9) to define the default parameters for all
routes at once. However, you typically do not use this key and define the defaults on a per-route
basis.
Above, the <route_name> placeholder should be the name of the route. A route name must be in
lower case, like home or about. The type key specifies the route class name. It can be either
the full class name, like ZendMvcRouterHttpLiteral, or a short alias, like Literal.
The optional priority key allows to define the priority (which should be an integer number)
of the route in the priority list (routes with higher priority will be visited first). If you omit the
priority key, the routes will be visited in the LIFO order.
Routes having equal priority will be visited in the LIFO order. Thus, for the best
performance, you should register routes that will match most often in the last turn,
and least common routes should be registered first.
The options key defines the array of routes options. We will discuss the options in the following
sections of this chapter.
113
URL Routing
If you need to organize the routes in a chain (degenerated subtree), you add the chain_routes
key to your route configuration:
'<route_name>' => array(
'type' => '<route_class>',
'priority' => <priority>,
'options' => array(
//...
),
'chain_routes' => array(
// Add chained routes here.
// ...
)
),
Looking at the two examples above, you wont see the explicit usage of Part and Chain
route types, because (for your convenience) they are used by the ZF2 automatically
when it encounters the child_routes and chain_routes keys in your routing configuration.
URL Routing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
return array(
'router' => array(
'routes' => array(
'home' => array(
'type' => 'Zend\Mvc\Router\Http\Literal',
'options' => array(
'route'
=> '/',
'defaults' => array(
'controller' => 'Application\Controller\Index',
'action'
=> 'index',
),
),
),
// The following is a route to simplify getting started creating
// new controllers and actions without needing to create a new
// module. Simply drop new controllers in, and you can access them
// using the path /application/:controller/:action
'application' => array(
'type'
=> 'Literal',
'options' => array(
'route'
=> '/application',
'defaults' => array(
'__NAMESPACE__' => 'Application\Controller',
'controller'
=> 'Index',
'action'
=> 'index',
),
),
'may_terminate' => true,
'child_routes' => array(
'default' => array(
'type'
=> 'Segment',
'options' => array(
'route'
=> '/[:controller[/:action]]',
'constraints' => array(
'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
'action'
=> '[a-zA-Z][a-zA-Z0-9_-]*',
),
'defaults' => array(
),
),
),
),
),
),
),
114
115
URL Routing
47
48
49
//...
);
This configuration corresponds to the tree route stack shown in figure 5.5:
In the configuration presented above, we have two routing rules listed in turn: first we have the
home route (line 5) and then we have the compound route (line 19), consisting of two routes.
The parent route is named application. You can see that under the child_routes subkey it has
the child rule named default. This compound route also has a name: it is the concatenation of
all names of routes it consists of (application/default).
The may_terminate parameter (line 29) of the parent route defines whether the child route must
always match or the child route is optional. If the may_terminate parameter is true and the
end of the URL is reached, the child route is ignored and the route is treated as matching. If
the may_terminate parameter is false and the last segment of the URL is reached, the route is
treated as non-matching.
In the next sections, we will provide some examples on how to use the route types in your web
site.
URL Routing
1
2
3
4
5
6
7
8
9
10
116
Line 2 of this example says that the routes type is Literal. The actual route matching algorithm
is implemented in the Zend\Mvc\Router\Http\Literal class. You can either specify the full class
name or its short alias (Literal).
Line 4 defines the route string to match against the URL path (the forward slash / means the
empty URL part). Because we have the literal route type, the route match is achieved only when
you have the exact literal path match. For example, if you have the URL http://localhost/ or
http://localhost, it will match the / route string.
Lines 5-8 define the defaults, which are the parameters returned by the router if the route
matches. The controller and action parameters define the controller and controllers action
method which should be executed. You can also define other parameters here, if needed.
As another example of the Literal route type, lets add the /about route for the About page
weve created earlier in the Views section of Chapter 4. To create the route, add the following
lines right after the home rule definition inside of your module.config.php file:
'about' => array(
'type' => 'Literal',
'options' => array(
'route' => '/about',
'defaults' => array(
'controller' => 'Application\Controller\Index',
'action'
=> 'about',
),
),
),
If you now open the http://localhost/about URL in your web browser, you should see the About
page.
117
URL Routing
If you look at the module.php.config file, you can see the Segment route type is used
inside of the application/default route to make actions of your controllers automatically mapped to site URLs. You just add an action method to your controller, and
it becomes available by a URL like http://localhost/<module>/<controller>/<action>.
For example, you can see the About page of your site with the following URL:
http://localhost/application/index/about.
To demonstrate the creation of the Segment route type, lets implement a controller action
which will generate a simple barcode image. Barcodes are widely used in supermarkets for
optically recognizing goods in your shopping cart. The barcodes may be of different types and
have different labels. We will use the Segment route type to map the action to a URL like
http://localhost/barcode/<type>/<label>.
First, we define the barcode routing rule in the module.config.php file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Segments of the route string (line 4) may be constant or variable. You can define the variable
segments by using wildcards. We have three segments: barcode, :type and :label. The
barcode segment is constant, while the latter two are wildcards (wildcards name should start
with a colon).
You specify how a wildcard should look like inside of the constraints subkey (lines 5-8). We
define the regular expression [a-zA-Z][a-zA-Z0-9_-]* which constraints our :type wildcard
to begin with a letter and contain one or several letters, digits, underscores or minus characters.
The constraint for the :label wildcard is almost the same, but this segment can start with any
allowed character (either letter, digit, underscore or minus sign character).
Optional segments can be enclosed in square brackets. In our example, we have both the :type
and :label segments as optional.
In lines 9-12, we define the defaults, the parameters that will be returned by the router. The
controller and action defaults specify which controller and action method to execute on route
match.
Next, we add the barcodeAction() method into the IndexController class:
118
URL Routing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
In lines 10-11 we get the values of the type and label wildcards from route. We do that with the
help of Params controller plugins fromRoute() method. Analogous to fromQuery() method, it
takes two arguments: the variable name and its default value.
For generating the barcode image, we use the Zend\Barcode component. In line 14 we define the
label text for the barcode. In lines 18-19 we create the Barcode object with the factory method.
Finally, in line 23 we render the image file by dumping it to PHP output stream.
Zend\Barcode is an auxiliary component used for generation of various barcode
images. For additional information about this component, please refer to the corresponding section of Zend Framework reference manual.
In line 26 we return the Response object to suppress the default view rendering.
Now, enter the http://localhost/barcode URL into your browser to see the barcode image
(shown in figure 5.6):
119
URL Routing
Please note that for barcode images to work, you need to have the GD extension of
the PHP engine installed and enabled. Please refer to Appendix A for instructions on
how to do that.
Because we have the wildcards in the route, you can pass the type and label parameters of the
barcode image in the URL. Below, several URL examples are provided (corresponding barcodes
are presented in figure 5.7):
a.
b.
c.
d.
e.
f.
g.
h.
http://localhost/barcode/code39/HELLO-WORLD
http://localhost/barcode/leitcode/12345
http://localhost/barcode/identcode/98765453212
http://localhost/barcode/postnet/123456
http://localhost/barcode/planet/1234567890123
http://localhost/barcode/upca/12345678901
http://localhost/barcode/code128/ABCDEF
http://localhost/barcode/ean2/12
120
URL Routing
Lets implement the route which will serve the static pages of the site. Because static pages
are simple, you typically wont need to add per-page action methods to the controller. All pages
will be handled by the single action IndexController::docAction().
First, we add the Regex route named doc to the module.config.php file:
1
2
3
4
5
6
7
8
9
10
11
Line 2 defines the Regex type for the route. In line 4, we have the regular expression /doc(?<page>\/[a-zA-Z0-9_\-]+)\.html. It will match to URLs like /doc/contents.html, /docs/introduction.html and so
on. The expression contains the named capture page, which will be returned by the router on
match together with the default parameters.
Line 9 contains spec option, which is used for generating URLs by route (we will discuss
generating URLs by route later in this chapter).
Next, add the following action to IndexController class:
In PHP PCRE regular expressions, it is possible to name a sub-pattern using the syntax (?P<name>pattern). This sub-pattern will then be
indexed in the matches array by its name.
URL Routing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
121
In lines 3-4 above, we retrieve the page parameter from route (remember the page named
capture from our regular expression?) and save it as the $pageTemplate variable. We will use the
$pageTemplate variable for determining the view template name to pass to the view resolver.
Then, in lines 6-10, we check if such a file name is present, and if not, return the 404 Not Found
status code, which will force ZF2 to display the error page. In line 12, we create the ViewModel
variable container, and in line 13 we explicitly set the view template name for rendering.
To see the documentation system in action, create a couple of static view template files: the
Table of Contents page (contents.phtml) and the Introduction page (introduction.phtml).
Create the doc subdirectory under the view/application/index directory of the Application
module and put the contents.phtml view template there:
1
2
3
4
5
6
7
8
9
10
<h1>Table of Contents</h1>
<ul>
<li>
<a href="<?php echo $this->url('doc',
array('page'=>'introduction')); ?>">
Introduction
</a>
</li>
</ul>
In the lines above, we provide the HTML code for the Table of Contents page header, and the
list which contains the single item named Introduction pointing to the Introduction static
page. The link URL is generated with the Url view helper (for more details on the Url helper, see
further sections in this chapter).
Then add the introduction.phtml page into the same doc directory:
122
URL Routing
1
2
3
<h1>Introduction</h1>
<p>Some introductory materials.</p>
In the lines above, we define the HTML markup for the simple Introduction page.
Now, if you open the http://localhost/doc/contents.html URL in your browser, you should see
a nice simple documentation system which you can extend and use in your site (figure 5.8):
Clicking the Introduction link will direct you to the Introduction static page. You can also add
other pages to the doc directory to make them automatically available for site users through our
Regex route.
One disadvantage of such a documentation system is that it does not work well if
you place nested pages in subdirectories under the doc directory. The reason of this
limitation lies in the way the Regex route assembles URLs. You cant generate URLs
containing slash characters, as these unsafe characters will be automatically URLencoded. We will work-around this problem with our custom route type that we will
create at the end of this chapter.
URL Routing
123
To demonstrate the usage of the Wildcard route type, lets assume we need to create an action
that would display blog post(s) published during some period of time. For this action, we would
like to use URLs looking like http://localhost/blog/year/2013/month/April/name/my-vacation.
First, lets add the blogAction() method to our IndexController class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
In the code above, we get $year, $month and $name parameters from route as usual (lines 3 - 5).
Because the parameters are optional, we echo different text to the screen based on which
parameters are present. If the user provided the name of the blog post to show, we display its
name, otherwise we display all blog posts for the period. If the user provided the year and month,
we display the blog post(s) for this year and month; if the user provided only the year, we display
blog posts for the year. If the user didnt provide neither year nor month, we display all posts for
the period.
Next, define the routing configuration for this action. Add the following lines under your router
key:
URL Routing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
124
The key_value_delimiter option defines the character which separates key/value pairs, typically you set this with slash (/) character, but you can use any other one. The param_delimiter
option defines the character which separates the key and value within the pair. Typically, this is
also the / character.
To see how the wildcard route works, type the following URL in your web browsers navigation bar: http://localhost/blog/year/2013/month/April/name/my-vacation. You should see
the following text in your browser: You requested to see the blog post named my-vacation
published in April 2013. If you enter the URL http://localhost/blog/year/2013, you should see
the following output: You requested to see all blog posts published in 2013.
URL Routing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
125
In the example above, in line 1 we define the route which has the Hostname type. The route
option (line 4) defines the domain name to match against. The :subdomain is a wildcard, which
can take different sub-domain values. The constraints key defines the regular expression this
sub-domain parameter must match. The Hostname route will differentiate your domains, so each
site will behave differently, depending on the value of the subdomain parameter returned:
// An example of an action that uses parameters returned by
// Hostname route.
public function someAction() {
// Get the 'subdomain' parameter from the route.
$subdomain = $this->params()->fromRoute('subdomain', null);
// Use different logic based on sub-domain.
//...
// Render the view template.
return new ViewModel();
}
The Scheme route type is useful if you need to handle HTTP and HTTPS protocols in different
ways.
The typical Scheme route configuration is presented below:
The HTTPS protocol is typically used for secure connections, like account page or shopping cart page. When you use HTTPS, the request
data is tunnelled through Secure Socket Layer (SSL) channel and not available to third parties.
URL Routing
1
2
3
4
5
6
7
8
9
10
11
12
126
Above, we define the route of type Scheme. It takes the scheme option, which should be the
scheme to match against (like http or https). If the scheme in HTTP requests URL is exactly
the same as the scheme option, the route is considered matching. You can use the defaults key
to return some parameters on route match. In the example above, the https boolean parameter
will be returned.
The Method route type can be used if you need to direct GET and POST requests into different
controllers actions. Its typical configuration is presented below:
1
2
3
4
5
6
7
8
9
10
11
Above, we define the route which has the Method type. It takes the verb option, which may be
the comma-separated list of acceptable HTTP verbs (like get, post, put, etc.)
127
URL Routing
To retrieve a parameter from the route in your controllers action method, you typically use the
Params controller plugin and its fromRoute() method, which takes two arguments: the name of
the parameter to retrieve and the value to return if the parameter is not present.
The fromRoute() method can also be used to retrieve all parameters at once as an array. To do
that, call the fromRoute() without arguments, as shown in the example below:
// An example action.
public function someAction() {
// Get the single 'id' parameter from route.
$id = $this->params()->fromRoute('id', -1);
// Get all route parameters at once as an array.
$params = $this->params()->fromRoute();
//...
}
Method Name
Description
getMatchedRouteName()
getParams()
getParam($name, $default)
In most cases, it will be sufficient to use the Params controller plugin, but alternatively
you can use the RouteMatch object for accomplishing the same task.
To get the RouteMatch object from your controllers action method, you can use the following
code:
URL Routing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
128
// An example action.
public function someAction() {
// Get the RouteMatch object.
$routeMatch = $this->getEvent()->getRouteMatch();
// Get matched route's name.
$routeName = $routeMatch->getMatchedRouteName();
// Get all route parameters at once as an array.
$params = $routeMatch->getParams();
//...
}
In line 5 of the code above, we use the getEvent() method of the AbstractActionController
base class to retrieve the MvcEvent object, which represents the event (in ZF2, the application life
cycle consists of events). We then use the the getRouteMatch() method of the MvcEvent class to
retrieve the RouteMatch object.
In line 8, we use the getMatchedRouteName() method to retrieve the name of the route that
matched the HTTP request, and in line 11 we retrieve all the parameters from the route.
The MvcEvent class can also be used for retrieving the router (the top-level route class). You can
do this with the getRouter() method of the MvcEvent class, as below:
// Call this inside of your action method
// to retrieve the RouteStackInterface for the router class.
$router = $this->getEvent()->getRouter();
In the code above, we use the getRouter() method, which returns the RouteStackInterface
interface. This interface is the base interface for both SimpleRouteStack and TreeRouteStack,
and it provides the methods for working with the routes contained inside the route stack.
129
URL Routing
tag having href attribute specifying the URL of the destination page. Below, an example of a
hyperlink pointing to an external page is presented:
<a href="http://example.com/path/to/page">A link to another site page</a>
When you generate a hyperlink to a resource internal to your site, you typically use relative URL
(without host name):
<a href="/path/to/internal/page">A link to internal page</a>
To generate URLs in your view templates (.phtml files), you can use the Url view helper class,
which takes the route name as an input argument:
1
2
3
4
5
In the lines above, we generate two relative URLs. In line 2, we call the Url view helper and
pass the home route name as its parameter. In line 5, we pass the about route name as an
argument for the Url view helper.
In the example above, the Url view helper internally uses the RouteMatch object and
calls the Literal route to assemble the URL string by route name.
After the PhpRenderer class executes the view templates code, the output HTML markup will
be the following:
<!-- A hyperlink to Home page -->
<a href="/">Home page</a>
<!-- A hyperlink to About page -->
<a href="/about">About page</a>
URL Routing
1
2
3
4
5
6
7
8
9
10
130
In the example above, we use Url view helper to generate the two URLs by route name and
parameters. We pass the application/default compound route name as the first argument, and
an array of parameters as the second argument.
In line 3, we pass the controller and action parameters to tell the Segment route class that it
should substitute the corresponding wildcards in the route string with the index and about
strings.
After the PhpRenderer class executes the view templates code, the output HTML markup will
be the following:
<!-- A hyperlink to About page -->
<a href="/application/index/about" > About page </a>
<!-- A hyperlink to Barcode image -->
<a href="/application/index/barcode/code39/HELLO-WORLD" > Barcode image </a>
As another example, lets try to generate a URL for our Regex route (the one which serves our
static pages):
<!-- A hyperlink to Introduction page -->
<a href="<?php echo $this->url('doc', array('page'=>'introduction')); ?>">
Introduction </a>
URL Routing
1
2
3
4
5
6
7
8
131
In lines 2-3 of the example above, we pass the home route name as the first argument, empty
array as the second argument, and an array containing force_canonical option as the third
argument. In lines 6-8, we also pass the force_canonical option as the third argument for
generating the URL of the About page.
The resulting HTML markup of the code above will be as follows:
<!-- A hyperlink to Home page -->
<a href="http://localhost/" > Home page </a>
<!-- A hyperlink to About page -->
<a href="http://localhost/application/index/about" > About page </a>
In the code above, we specified the query option, which is the array containing namevalue
pairs of the query parameters.
132
URL Routing
1
2
3
4
5
6
7
8
9
10
11
12
The arguments the Url plugin takes and their meaning are identical to the Url view
helpers ones. So, you can generate absolute or relative URLs the same way you did in
your view templates.
We could assume it generates the URL like /doc/chapter1/introduction.html. But because the
slash (/) character is unsafe, it will be replaced with the %2F characters for security reasons,
and we will have the following HTML code:
<!-- A hyperlink to Introduction page -->
<a href="/doc/chapter1%2Fintroduction.html"> Introduction </a>
Unfortunately, this hyperlink is unusable, because it wont match our Regex route.
133
URL Routing
5.12.1 RouteInterface
We know that every route class must implement the Zend\Mvc\Router\Http\RouteInterface
interface. The methods of this interface are presented in table 5.4:
Table 5.4. RouteInterface methods
Method Name
Description
factory($options)
match($request)
assemble($params, $options)
getAssembledParams()
The static factory() method is used by the ZF2 router (TreeRouteStack or SimpleRouteStack)
for instantiating the route class. The router passes the options array an argument for the
factory() method.
The match() method is used to perform the matching of the HTTP request (or, particularly its
URL) against the options data passed to the route class through the factory(). The match()
method should return either an instance of the RouteMatch class on successful match, or null
on failure.
The assemble() method is used for generating URL string by route parameters and options. The
getAssembledParams() helper methods purpose is to return the array of parameters which were
used on URL generation.
134
URL Routing
will be URL-encoded making the hyperlink unusable). We will create our custom StaticRoute
class that allows to fix this issue.
Moreover, the class we will create is more powerful, because it will not only recognize URLs
starting with /doc and ending with .html, but it will also recognize generic URLs, like /help
or /support/chapter1/introduction.
What we want to achieve:
The StaticRoute class should be insertable to the route stack (to SimpleRouteStack or to
TreeRouteStack) and usable together with other route types.
The route class should recognize generic URLs, like /help or /introduction.
The route class should match the URL against the directory structure. For example, if
the URL is /chapter1/introduction, then the route should check if the corresponding
view template file <base_dir>/chapter1/introduction.phtml exists and is readable, and if
so, report match. If the file does not exist (or not readable), return the failure status.
The route class should check the URL for acceptable file names using a regular expression.
For example, the file name introduction is acceptable, but the name *int$roduction is
not. If the file name is not acceptable, the failure status should be returned.
The route should be able to assemble the URL string by route name and parameters.
To start, create the Service subdirectory under the modules source directory and put the
StaticRoute.php file inside of it (figure 5.9). Inside that file, paste the stub code presented below:
URL Routing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
135
<?php
namespace Application\Service;
use
use
use
use
use
use
Traversable;
\Zend\Mvc\Router\Exception;
\Zend\Stdlib\ArrayUtils;
\Zend\Stdlib\RequestInterface as Request;
\Zend\Mvc\Router\Http\RouteInterface;
\Zend\Mvc\Router\Http\RouteMatch;
From the code above, you can see that we placed the StaticRoute class inside the Application\Service
namespace (line 2), because it can be treated as a service model.
In lines 4-9, we define some class name aliases for making the class names shorter.
In lines 11-28, we define the stub for the StaticRoute class. The StaticRoute class implements
the RouteInterface interface and defines all the methods specified by the interface: factory(),
match(), assemble() and getAssembledParams().
Next, lets add several protected properties and the constructor method to the StaticRoute class,
as shown below:
URL Routing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
136
<?php
//...
class StaticRoute implements RouteInterface
{
// Base view directory.
protected $dirName;
// Path prefix for the view templates.
protected $templatePrefix;
// File name pattern.
protected $fileNamePattern = '/[a-zA-Z0-9_\-]+/';
// Defaults.
protected $defaults;
// List of assembled parameters.
protected $assembledParams = array();
// Constructor.
public function __construct($dirName, $templatePrefix,
$fileNamePattern, array $defaults = array())
{
$this->dirName = $dirName;
$this->templatePrefix = $templatePrefix;
$this->fileNamePattern = $fileNamePattern;
$this->defaults = $defaults;
}
// ...
}
Above, in line 7, we define the $dirName property that is intended for storing the name of
the base directory where the static view templates will be located. In line 10, we define the
$templatePrefix class variable for storing the prefix for prepending to all view template names.
Line 13 contains the $fileNamePattern variable that will be used for checking the file name.
In lines 22-29, we define the constructor method that is called on instance creation for initializing
the protected properties.
Next, lets implement the factory() method for our StaticRoute custom route class. The
factory() method will be called by the router for instantiating the route class:
URL Routing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
137
<?php
//...
class StaticRoute implements RouteInterface
{
//...
// Create a new route with given options.
public static function factory($options = array())
{
if ($options instanceof Traversable) {
$options = ArrayUtils::iteratorToArray($options);
} elseif (!is_array($options)) {
throw new Exception\InvalidArgumentException(__METHOD__ .
' expects an array or Traversable set of options');
}
if (!isset($options['dir_name'])) {
throw new Exception\InvalidArgumentException(
'Missing "dir_name" in options array');
}
if (!isset($options['template_prefix'])) {
throw new Exception\InvalidArgumentException(
'Missing "template_prefix" in options array');
}
if (!isset($options['filename_pattern'])) {
throw new Exception\InvalidArgumentException(
'Missing "filename_pattern" in options array');
}
if (!isset($options['defaults'])) {
$options['defaults'] = array();
}
return new static(
$options['dir_name'],
$options['template_prefix'],
$options['filename_pattern'],
$options['defaults']);
}
In the code above, we see that the factory() method takes the options array as the argument
(line 9). The options array may contain the options for configuring the route class. The
StaticRoute class will accept the following options:
URL Routing
138
dir_name - the base directory where to store all static view templates.
template_prefix - the prefix to prepend to all template names.
filename_pattern - the regular expression for checking the file names.
defaults - parameters returned by router by default.
Once we parsed the options, in lines 37-41 we call the class constructor method to instantiate
and return the StaticRoute object.
The next method we add to the StaticRoute route class is the match() method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
//...
class StaticRoute implements RouteInterface
{
//...
// Match a given request.
public function match(Request $request, $pathOffset=null)
{
// Ensure this route type is used in an HTTP request
if (!method_exists($request, 'getUri')) {
return null;
}
// Get the URL and its path part.
$uri = $request->getUri();
$path = $uri->getPath();
if($pathOffset!=null)
$path = substr($path, $pathOffset);
// Get the array of path segments.
$segments = explode('/', $path);
// Check each segment against allowed file name template.
foreach ($segments as $segment) {
if(strlen($segment)==0)
continue;
if(!preg_match($this->fileNamePattern, $segment))
return null;
}
// Check if such a .phtml file exists on disk
$fileName = $this->dirName . '/'.
$this->templatePrefix.$path.'.phtml';
URL Routing
37
38
39
40
41
42
43
44
45
46
47
48
49
50
139
if(!is_file($fileName) || !is_readable($fileName)) {
return null;
}
$matchedLength = strlen($path);
// Prepare the RouteMatch object.
return new RouteMatch(array_merge(
$this->defaults,
array('page'=>$this->templatePrefix.$path)
),
$matchedLength);
}
}
In the code above, we see that the match() method takes two arguments: the HTTP request object
(an instance of Zend\Stdlib\Request class) and the URL path offset. The request object is used
for accessing the request URL (line 17). The path offset parameter is a non-negative integer,
which points to the portion of the URL the route is matched against (line 21).
In line 24, we extract the segments from URL. Then we check if every segment is an acceptable
file (directory) name (lines 27-321). If the segment is not a valid file name, we return null as a
failure status.
In line 35, we calculate the path to the view template, and in lines 27-29 we check if such a
file really exists and accessible for reading. This way we match the URL against the directory
structure.
In lines 44-48, we prepare and return the RouteMatch object with the default parameters plus the
page parameter containing the view template name for rendering.
To complete the implementation of our StaticRoute class, we add the assemble() and getAssembledParams()
methods, that will be used for generation of URLs by route parameters. The code for these
methods is presented below:
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
//...
class StaticRoute implements RouteInterface
{
//...
// Assembles a URL by route params
public function assemble(array $params = array(),
array $options = array())
{
$mergedParams = array_merge($this->defaults, $params);
$this->assembledParams = array();
URL Routing
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
140
if(!isset($params['page'])) {
throw new Exception\InvalidArgumentException(__METHOD__ .
' expects the "page" parameter');
}
$segments = explode('/', $params['page']);
$url = '';
foreach($segments as $segment) {
if(strlen($segment)==0)
continue;
$url .= '/' . rawurlencode($segment);
}
$this->assembledParams[] = 'page';
return $url;
}
// Get a list of parameters used while assembling.
public function getAssembledParams()
{
return $this->assembledParams;
}
}
In the code above, we define the assemble() method, which takes the two arguments: the
parameters array and the options array (line 10). The method constructs the URL by encoding
the segments with URL encoding and concatenating them (line 20-26).
The method getAssembledParams() just returns the names of the parameters we used for URL
generation (page 36).
Now weve finished the StaticRoute route class. To use our custom route type, we add the
following configuration to the module.config.php configuration file:
1
2
3
4
5
6
7
8
9
10
11
12
URL Routing
141
In line 1 of the configuration above, we define the routing rule named static. The type
parameter defines the full StaticRoute class name for the rule (line 2). In the options array, we
define the base directory where the static pages will be placed (line 4), the template prefix (line
5), the filename pattern (line 6), and the defaults array, containing the name of the controller
and the action that will serve all the static pages.
The final step is creating the action method in the IndexController class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
The action above is almost identical to the action we used for the Regex route. In line 4, we
retrieve the page parameter from route and save it as the $pageTemplate variable. In line 121,
we create the ViewModel variable container, and in line 12 we explicitly set the view template
name for rendering.
To see the system in action, lets add a couple of static view pages: the Help page (help.phtml)
and the introduction page (intro.phtml). Create the static subdirectory under the view/application/index directory of the Application module and put the help.phtml view template there:
1
2
3
4
5
6
<h1>Help</h1>
<p>
See the help <a href="<?php echo $this->url('static',
array('page'=>'/chapter1/intro')); ?>">introduction</a> here.
</p>
Then create the chapter1 subdirectory in the static directory and put the following chapter1/intro.phtml file in there:
142
URL Routing
<h1>Introduction</h1>
<p>
Write the help introduction here.
</p>
Finally, you should receive the following directory structure (see figure 5.10):
Eventually, open the following URL in your browser: http://localhost/help. The Help page should
appear (see figure 5.11 for example). If you type the http://localhost/chapter1/intro URL in your
browser, you should see the Introduction page (figure 5.12).
143
URL Routing
You can create static pages just by adding the phtml files under the static directory, and they will
automatically become available to site users.
If you are stuck, you can find this complete working example inside the Hello World
application.
5.13 Summary
In this chapter, weve learned about routing. Routing is used for mapping HTTP request to
controllers action method. There are several route types (Literal, Segment, Regex, Hostname,
Scheme, Method etc.). Each route type uses different URL segments (and, possibly, other HTTP
requests data) to compare the URL with the specified route template. We also learned how to
write custom route class if the capabilities of standard route types are not sufficient.
The main task of a route class is to return a route match containing the set of parameters, by
which a controller and action can be determined. An opposite task a route class allows to do is
generating a URL by parameters. This feature is widely used in view layer of the application for
generating hyperlinks.
Route types can be combined in a nested tree with the help of TreeRouteStack router, or
organized in a chain with SimpleRouteStack router. These two routers allow to define arbitrarily
complex rules.
Routing configuration is stored in modules configuration file under the router key. Each
module exposes its own routing rules, which are merged with other modules configuration
upon application start up.
Description
Zend\Mvc
Support of MVC pattern. Implements base controller classes, controller plugins, etc.
Zend\View
Implements the functionality for variable containers, rendering a web page and
common view helpers.
145
It provides the CSS reset that is a style sheet defining styles for all possible HTML elements.
This ensures your web site will look the same way in all web browsers.
It provides the base CSS rules that define style of typography (headings and text), tables,
forms, buttons, images and so on.
It defines the grid system. The grid system allows to arrange elements on your web page
in a grid-like structure. For example, look at the Skeleton Applications main page (figure
6.1), where we have the grid consisting of three columns.
It defines useful web interface components like dropdown menus, navigation bars, breadcrumbs, pagination and so on. For example, on the skeleton apps main page, there is
the navigation bar component at the top, and the header (also called the Hero Unit or
Jumbotron) component below the navbar. These components are very handy on any web
site.
In includes the JavaScript extensions that allow to make Bootstrap-provided interface
components more interactive. For example, JavaScript is used to animate dropdown menus
and display modal dialogs.
Figure 6.1. Main page of the skeleton app and its layout
If you are new to Twitter Bootstrap, it is recommended that you refer to Appendix
C, where you can find more information about using Twitter Bootstrap and its
components.
146
<!DOCTYPE html>
<html lang="en">
<head>
<title>Welcome</title>
<!-- Include metas, stylesheets and scripts here -->
</head>
<body>
<!-- Include page content here -->
</body>
</html>
The <head> element contains the page title text, meta information and references to included
stylesheets and scripts. The <body> element contains the content of the page, like the logo image,
the navigation bar, the page text, and the footer with copyright information.
In Zend Framework 2, you define this common structure with the master view template called
the layout. The layout decorates other view templates.
The layout template typically has a placeholder in which ZF2 puts the content specific to a
particular page (see figure 6.2 for example).
In the Skeleton Application, the default layout template file is called layout.phtml and is located
inside of the view/layout directory in Application modules directory (see figure 6.3 for example).
147
Lets look at the layout.phtml template file in more details. Below, the complete contents of the
file is presented (because some lines of the file are too long for a book page, line breaks are
inserted where necessary):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
148
73
74
75
149
You can see that the layout.phtml file (as a usual view template) consists of HTML tags
mixed with PHP code fragments. When the template is rendered, ZF2 evaluates the inline PHP
fragments and generates resulting HTML page visible to site users.
Line 1 above generates the <!DOCTYPE> declaration of the HTML page with the Doctype view
helper.
Line 3 defines the <html> element representing the root of the HTML document. The <html> tag
is followed by the <head> tag (line 4), which typically contains a title for the document, and can
include other information like scripts, CSS styles and meta information.
In line 5, the <meta> tag provides the browser with a hint that the document is encoded using
UTF-8 character encoding.
In line 6, we have the HeadTitle view helper that allows to define the title for the page
(ZF2 Skeleton Application). The title will be displayed in the web browsers caption. The
setSeparator() method is used to define the separator character for the compound page titles;
the setAutoEscape() method enhances the security by escaping unsafe characters from the page
title. The Translate view helper is used for localizing your web sites strings into different
languages.
In line 12, the HeadMeta view helper allows to define the <meta name="viewport"> tag containing
meta information for the web browser to control layout on different display devices, including
mobile browsers. The width property controls the size of the viewport, while the initial-scale
property controls the zoom level when the page is loaded. This makes the web page layout
responsive to device viewport size.
In line 19, the HeadLink view helper allows to define the <link> tags. With the <link> tags, you
typically define the favicon for the page (located in APP_DATA\public\img\favicon.ico file)
and the CSS stylesheets.
In lines 22-24, the stylesheets common to all site pages are included by the prependStylesheet()
method of the HeadLink view helper. Any page in our web site will load three CSS stylesheet
files: bootstrap.min.css (the minified version of Twitter Bootstrap CSS Framework), bootstraptheme.min.css (the minified Bootstrap theme stylesheet) and style.css (CSS file allowing us to
define our own CSS rules overriding Bootstrap CSS rules).
Lines 27-35 include the JavaScript files that all your web pages will load. The scripts are executed
by the clients web browser, allowing to introduce some interactive features for your pages.
We use the the bootstrap.min.js (minified version of Twitter Bootstrap) and jquery.min.js
(minified version of jQuery library) scripts. All scripts are located in APP_DIR/public/js directory.
The <!DOCTYPE> declaration goes first in the HTML document, before the <html> tag. The declaration provides an instruction to the web
browser about what version of HTML the page is written in (in our web site, we use HTML5-conformant document type declaration).
The UTF-8 allows to encode any character in any alphabet around the world, thats why it is recommended for encoding the web pages.
A compound page title consists of two parts: the first part (Zend Skeleton Application) is defined by the layout, and the second part defined by a particular page - is prepended to the first one. For example, for the About page of your site you will have the About - Zend Skeleton
Application, and for the Documentation page you will have something like Documentation - Zend Skeleton Application.
150
Line 38 defines the <body> tag, the documents body which contains all the contents of the
document, such as the navigation bar, text, hyperlinks, images, tables, lists, etc.
In lines 39-63, you can recognize the Bootstrap navigation bar definition. The skeleton application
uses the collapsible navbar with dark inverse theme. The navbar contains the single link Home.
If you look at lines 63-72, you should notice the <div> element with container class which
denotes the container element for the grid system. So, you can use the Bootstrap grid system to
arrange the contents of your pages.
Line 64 is very important, because this line defines the inline PHP code that represents the page
content placeholder we talked about in the beginning of this section. When the ZF2 page renderer
evaluates the layout template, it echoes the actual page content here.
Lines 65-71 define the page footer area. The footer contains the copyright information like 2013
by Zend Technologies Ltd. All rights reserved. You can replace this information with you own
company name.
Line 73 is the placeholder for JavaScript scripts loaded by the concrete page. The InlineScript
view helper will substitute here all the scripts you register (about registering JavaScript scripts,
you will see it later in this chapter).
And finally, lines 74-75 contain the closing tags for the body and the HTML document.
Next, we will use the Bootstrap-provided grid system for arranging the main blocks on the page.
Replace the HTML code of the <body> element (lines 37-73) with the following one:
151
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<body>
<div class="container">
<div class="row">
<!-- Page header -->
<div class="col-md-4">
<div class="app-caption">Hello World!</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Navigation bar -->
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Breadcrumbs -->
</div>
</div>
<div class="row">
<div class="col-md-12">
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
152
In the code above, we defined the <div> element with the container class and put the <div>
elements of the grid inside of it. The grid consists of 5 rows:
The page header containing the Hello World! text (lines 3-8). The header text spans four
grid columns. For styling the text, we use our custom CSS class app-caption (we will
define this class in style.css file a little bit later).
We left the space for navigation bar interface component in line 11.
In line 16, we have the space for breadcrumbs component.
In line 22, we have the page content placeholder. When the renderer evaluates the page, it
will echo the value of the $content variable, so the actual page content will be substituted
here.
And in lines 25-32, we provided the page footer with the text (c) 2013 by Your Company.
All rights reserved. You can change this text and substitute your company name here, if
you wish.
Next, we put the navigation bar in the corresponding grid row:
<!-- Navigation bar -->
<nav class="navbar navbar-default" role="navigation">
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="<?php echo $this->url('home') ?>">Home</a>
</li>
<li>
<a href="<?php echo $this->url('application/default',
array('controller'=>'index', 'action'=>'downloads')) ?>">
Downloads
</a>
153
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Support <b class="caret"></b>
<ul class="dropdown-menu">
<li>
<a href="<?php echo $this->url('doc',
array('page'=>'contents')) ?>">
Documentation
</a>
</li>
<li>
<a href="<?php echo $this->url('static',
array('page'=>'help')) ?>">
Help
</a>
</li>
</ul>
</a>
</li>
<li>
<a href="<?php echo $this->url('about') ?>">About</a>
</li>
</ul>
</div>
</nav>
In the code above, we used the navbar interface component provided by the Bootstrap. We also
used the Url view helper to insert the links to the navigation items.
We discussed the usage of the Url view helper in the Generating URLs from Route
section in Chapter 5.
Finally, we need to provide a couple of custom CSS rules to fine-tune the look and feel. We define
our own CSS rules in the style.css stylesheet.
We want to make the Hello World! header text to use larger bold font and use a nice looking
color. To do this, open the style.css file, and append the following lines to the end:
154
div.app-caption {
padding: 25px 0px;
font-size:3.0em;
font-weight: bold;
color:#6aacaf
}
In the CSS code above, we created the app-caption class which can be applied to <div> element
and defining the 25 pixels vertical padding, large font size, bold text style and the hexadecimal
representation of the RGB text color.
By default, in skeleton application, the navbar is pinned to page top, and the CSS rule for the page
body defines the 20 pixels top padding to leave space for it. Since in our Hello World example
weve unpinned the navigation bar from top of the page and placed it in page flow, we need to
remove the padding from page body top. To do that, edit the following CSS rule in the style.css
file and make it look like the one below:
body {
padding-bottom: 40px;
}
Great, weve completed the page layout template! To see the result of our changes, open the site
in your browser, you should see the page as in figure 6.4. You can click the links in navigation
bar to visit the pages like About or Documentation, etc. The content of a particular page is put
into the content placeholder of our layout.
The result can be seen in action in the Hello World sample application that is part of
this books example code available on GitHub.
1
2
3
4
5
6
7
8
9
10
11
155
In the example action method above, we use the Layout controller plugin (line 8) that allows
to access the instance of the ViewModel class associated with the layout template. To change the
layout template for this particular action method, we called the setTemplate() method provided
by the ViewModel class.
In addition to the Layout controller plugin, there is the Layout view helper which
provides the same capabilities. With the Layout view helper, you can, for example,
switch layout from the static page which has no specific controller action.
156
$this->layout()->setTemplate('layout/layout2');
// Return the response
return $response;
}
}
157
),
);
return new ViewModel(array(
'products' => $products
));
}
The action method above just prepares an array of products for rendering and passes it to the
view template with the help of the ViewModel variable container.
Next, add the partial-demo.phtml template file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
$this->headTitle('Partial View Demo');
?>
<h1>Partial View Demo</h1>
<p>
Below, the table of products is presented. It is rendered with the help of
partial views.
</p>
<table class="table table-striped table-hover">
<tr>
<th>ID</th>
<th>Product</th>
<th>Price</th>
</tr>
<?php
foreach($this->products as $product) {
echo $this->partial('application/index/table-row',
array('product'=>$product));
}
?>
</table>
In the view template above, we define the markup for the table of products (lines 10-23). In line
18, we walk through the items of the products array and render each row with the Partial view
helper.
The first argument of the Partial view helper is the name of the partial view template file
(application/index/table-row).
The second argument of the Partial view helper should be an array of arguments passed to
the view template. They will be accessible the same way as if you would pass them with the
ViewModel variable container.
Finally, create the table-row.phtml view template, which will be used as the partial view template:
158
<tr>
<td> <?php echo $this->product['id'] ?> </td>
<td> <?php echo $this->product['name'] ?> </td>
<td> <?php echo $this->product['price'] ?> </td>
</tr>
In the view template above, we just render a single row of the table.
To see the resulting web page, type http://localhost/application/index/partialdemo URL in your
browsers navigation bar. You should see something like in figure 6.5.
159
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
160
In the code above, we call the captureStart() method (line 1) and captureEnd() method (line
15) of the Placeholder view helper to delimit the HTML markup that will be captured by the view
helper and stored in its internal storage (instead of rendering to PHP standard output stream).
In lines 3-14, we put the markup of the inherited layout. The derived layout uses the two-cell
grid. The first cell of the grid (spanning 8 columns) will contain the actual content of a certain
page, and the second cell (spanning 4 columns) will contain advertisements. For styling the ads,
we utilize the Panel interface component provided by the Twitter Bootstrap.
In line 16, we use the Partial view helper which is used to render the parent layout
(layout.phtml). We pass the content captured by the Placeholder view helper to the Partial
view helper as the second argument.
This way, we produced the nice-looking layout which inherits the default layout and improves
the code reusability.
Now, if you set the layout2.phtml for all actions of, say Index controller, you should be able to
see the result as in figure 6.6.
161
<script type="text/javascript">
// Show a simple alert window with the "Hello World!" text.
$( document ).ready(function() {
alert('Hello World!');
});
</script>
In the example above, we created the <script> element, and put the jQuery callback function
in it. The jQuery binds a function to be executed when the DOM has finished loading. When
the function is executed, a simple alert window with the Hello World! text and OK button will
appear.
Since you put this JavaScript code inside the HTML file, we will refer to it as inline script. An
alternative way of storing JavaScript code is putting it in an external .js file. External files
typically contain code that is designed to be used by several web pages. Typically, external
JavaScript files are stored in APP_DIR/public/js/ directory. To link an external JS file to your
HTML page, you add the <script> element like below:
<script type="text/javascript" src="/js/jquery.min.js"></script>
When the browser encounter such a <script> element, it reads the external JS file and executes
the code.
Generally, there are two places inside an HTML file where you can put the script:
162
JavaScript code can be put in the <head> section of an HTML page. This method is
recommended to use when you need JavaScript to be loaded before the content of the
page. We used this method for loading the Twitter Bootstrap JavaScript extensions and
jQuery library.
Script can be placed at the bottom of the <body> section of an HTML page, just before the
closing </body> tag. This way is acceptable when you need the entire DOM to be loaded
before the script can start executing.
If a certain JavaScript file needs to be used on all (or on most) of the web pages, it is better to
place it in layout view template. But when a script needs to be used on a single page only, putting
it in the layout template is not the best idea. If you put such a script to layout template, the script
will be loaded to all pages, which can produce an unneeded traffic and increase page load time
for the whole site. To avoid this, you can add such a script for the desired page only.
To add a page-specific script which will be put in the <head> section of the web page, you use
the HeadScript view helper. Its methods are summarized by table 6.1:
Table 6.1. Methods provided by the HeadScript view helper
Method name
Description
appendFile()
offsetSetFile()
prependFile()
setFile()
appendScript()
offsetSetScript()
prependScript()
setScript()
To add a link to external JS file to the <head> section, of a page, you add the following PHP code
in the beginning of your view template (.phtml) file:
<?php
$this->headScript()->appendFile('/js/yourscript.js', 'text/javascript');
In the code above, we called the appendFile() method of the HeadScript view helper. This
method takes two arguments. The first one is the path to external JS file (if the file is stored
inside of APP_DIR/public/js directory, or an URL of a JS file if the file is located on another web
server). The second argument is the type of the script (it is typically equal to text/javascript).
Other methods provided by HeadScript view helper (such as prependFile(), offsetSetFile()
and setFile() differentiate only in the position in the list of scripts into which the new script
will be inserted.
The DOM (Document Object Model) is a convenient representation of an HTML document structure as a tree of elements.
163
In the example above, we used the PHPs Heredoc syntax to fill in the $script variable with
the inline JavaScript code. Then we call the appendScript() function on the InlineScript view
helper and pass the code as its argument.
But, using the InlineScript view helper may be not very convenient in sense of readability.
Moreover, NetBeans IDE syntax checker will be stuck on the Heredoc notation and will not
recognize the JavaScript code. To fix this, you can simply put the <script> element at the bottom
of your view template, as shown in the example below:
<!-- Page content goes first -->
<!-- Inline script goes last -->
<script type="text/javascript">
$( document ).ready(function() {
// Show a simple alert window with the "Hello World!" text.
alert("Hello World!");
});
</script>
This ensures the same effect is achieved as with InlineScript view helper, but allows for better
script readability and automatic syntax checking.
For HeadScript and InlineScript view helpers to work, you should ensure their
content is echoed in layout view template (look at lines 27 and 72 of layout.phtml file).
If you remove those lines from the layout template, the scripts wont be inserted in the
web page.
The name InlineScript does not fully reflect the capabilities of this view helper. Actually, it can insert both inline and external scripts.
The better name for this view helper would be BodyScript, because it is intended for inserting scripts in document body.
Heredoc is an alternative string definition method provided by PHP. It works well with multi-line strings.
164
6.7.1 Example
For a real-life example of inserting a JavaScript code in your web page, lets add a page with autocomplete feature. With this feature, the web browser will predict a word or phrase that the user
wants to type in by several first letters, without the user actually entering the text completely. We
can use an auxiliary JavaScript library called Twitter Typeahead. Analogous to Twitter Bootstrap,
the Typeahead library was developed in Twitter Inc. for their internal purposes and is distributed
freely.
Download typeahead.min.js file (a minified version of the Typeahead library) from the official
project page. When the download is finished, place the file in your APP_DIR/public/js directory.
Then add the typeahead.phtml file in your application/index/static subdirectory under the
modules view directory. This directory is served by the StaticRoute route type that weve
created and configured earlier in Chapter 5, and all static pages placed here will automatically
become available to site users.
In the typeahead.phtml view template file, put the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
$this->headTitle('Typeahead');
// Add a JavaScript file
$this->headScript()->appendFile('/js/typeahead.min.js', 'text/javascript');
?>
<h1>Typeahead</h1>
<p>Type a continent name (e.g. Africa) in the text field below:</p>
<input type="text" class="typeahead" title="Type here"/>
<script type="text/javascript">
$( document ).ready(function() {
$('input.typeahead').typeahead({
name: 'continents',
local: [
'Africa',
'Antarctica',
'Asia',
'Europe',
'South America',
'North America'
]
});
});
</script>
In the code above, we set the title for the page (line 2), then we append the typeahead.min.js file
to the <head> section of the page with the HeadScript view helper (line 4).
http://twitter.github.io/typeahead.js/
165
In line 9, we create a text input field where the user will be able to enter some text. We mark the
input field with the typeahead CSS class.
Lines 11-25 contain inline JavaScript code placed at the bottom of the view template (we dont
use InlineScript view helper for better code readability).
In line 12, we have the jQuery event handler bound to the document is ready event. This event
is fired when the complete DOM tree has been loaded.
In line 13, we have the jQuery selector (input.typeahead) which selects all input fields marked
with the typeahead CSS class and execute the typeahead() function on them.
The typeahead() function binds the change event handler to the text input field. Once the user
enters a character in the field, the handler is executed and checks the letters entered. It then
displays the dropdown menu with suggested auto-completion variants.
The typeahead() function takes two arguments: the name argument identifies the dataset, and
the local argument is a JSON array containing the available auto-completion variants.
To give the auto-completion field and its dropdown menu a nice-looking visual appearance, add
the following CSS rules to your style.css file.
.typeahead,
.tt-query,
.tt-hint {
width: 396px;
height: 30px;
padding: 0px 12px;
font-size: 1.1em;
border: 2px solid #ccc;
border-radius: 4px;
outline: none;
}
.tt-dropdown-menu {
width: 422px;
margin-top: 12px;
padding: 8px 0;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.tt-suggestion {
padding: 3px 20px;
font-size: 1.1em;
line-height: 24px;
}
166
.tt-suggestion.tt-is-under-cursor {
color: #fff;
background-color: #0097cf;
}
.tt-suggestion p {
margin: 0;
}
To see the auto-completion feature in work, type the http://localhost/typeahead URL in your
browser and press Enter. The Typeahead page will appear with the prompt to enter a continent
name. For example, type a letter to see how Typeahead suggests you available variants (figure
6.7).
167
You can see this example working in the Hello World sample bundled with this book
by typing the URL http://localhost/typeahead in your browser.
External CSS stylesheets are recommended to store the CSS rules. For example, the base CSS rules
provided by Twitter Bootstrap CSS framework are loaded from bootstrap.min.css and bootstraptheme.min.css files. Custom site-specific CSS rules can be stored in style.css file. Since you need
this CSS stylesheets for most of your pages, it is better to link them in the head section of the
layout template. But, if a certain CSS stylesheet needs to be loaded for a single page only, you
place it on that pages view template.
To add an external CSS stylesheet to a view template, you use the HeadLink view helper:
1
2
3
4
<?php
$this->headLink()->appendStylesheet('/css/style.css');
$this->headLink()->appendStylesheet(
'http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css');
In the example code above, we used the appendStylesheet method of the HeadLink view helper
to add an external CSS stylesheet to the head section of the document. The method accepts a
path to local CSS file (line 2) or a URL to CSS file located on another server (line 3).
The summary of HeadLink view helpers methods is provided in table 6.2).
168
Method name
Description
appendStylesheet()
offsetSetStylesheet()
prependStylesheet()
setStylesheet()
If you want to add an inline <style> element in the head section of the document, you can use
the HeadStyle view helper. Its methods are presented in table 6.3 below:
Table 6.3. Methods of the HeadStyle view helper
Method name
Description
appendStyles()
offsetSetStyle()
prependStyle()
setStyle()
6.8.1 Example
To demonstrate how to add a CSS stylesheet to your web page, we will take a real-life example.
Assume you need to let the user the ability to type a date (in YYYY-MM-DD format) in a text
input field. You would like to improve user experience by not just letting him to type the date,
but also by selecting it from a pop-up date-picker widget.
To achieve this goal, you can use a third-party library called jQuery UI . To integrate jQuery
UI in your page, you need to download two files from the official project page:
jquery-ui.min.js the minified version of jQuery UI JavaScript code;
jquery-ui.min.css the minified version of jQuery UI theming styles.
Put the jquery-ui.min.js file to APP_DIR/public/js, and jquery-ui.min.css file to APP_DIR/public/css. Finally, add the datepicker.phtml view template:
jQuery UI provides a set of user interface interactions, effects, widgets, and themes; it is based on jQuery library. jQuery UI is analogous
to Twitter Bootstrap in the sense that both provide reusable user interface components.
http://jqueryui.com/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
169
<?php
$this->headTitle('Datepicker');
$this->headScript()->appendFile('/js/jquery-ui.min.js', 'text/javascript');
$this->headLink()->appendStylesheet('/css/jquery-ui.min.css');
?>
<h1>Datepicker</h1>
<p>
Click the edit box below to show the datepicker.
</p>
<input type="text" class="datepicker" title="Type here"/>
<script>
$( document ).ready(function() {
$( "input.datepicker" ).datepicker({ dateFormat: 'yy-mm-dd' });
});
</script>
In the example above, we use the HeadScript view helpers appendFile() method (line 4) to add
the link to jquery-ui.min.js file to the head section of the document.
In line 5, we used the HeadLink view helpers appendStylesheet() method to add the link to
jquery-ui.min.css CSS stylesheet to the head section of the document.
In line 8, we added the text input field which will be used to enter the date.
In line 10-14, we added an inline JavaScript code for binding jQuery event handler to the text
input field. When the user clicks the text input field, the datepicker widget will appear allowing
to select the date.
To see the result, enter the http://localhost/datepicker URL into your browsers navigation bar
(see figure 6.8 for example).
170
171
6.9.1 Menu
First, lets implement the Menu view helper class that will render the HTML code of the navigation
bar. The Menu class will provide several methods allowing to set menu items in a form of array,
set the active menu item and render the menu (see table 6.4 for method summary).
Table 6.4. Methods of the Menu view helper
Method name
Description
__construct($items)
setItems($items)
setActiveItemId($activeItemId)
render()
renderItem($item)
Class constructor.
Method for setting the menu items.
Method for setting the currently active menu item.
Renders the menu.
Renders a single menu item.
The information describing a single menu item will be represented by an array like below (for
example, the Home item will have an ID, text label and an URL for a hyperlink):
array(
'id' => 'home',
'label' => 'Home',
'link' => $this->url('home')
)
We also want to add the support for dropdown menus as navigation items. For example, in case of
the Support dropdown menu having the Documentation and Help sub-items, the item description
will take the following form:
array(
'id' => 'support',
'label' => 'Support',
'dropdown' => array(
array(
'id' => 'documentation',
'label' => 'Documentation',
'link' => $this->url('doc', array('page'=>'contents'))
),
array(
'id' => 'help',
'label' => 'Help',
'link' => $this->url('static', array('page'=>'help'))
)
)
)
172
We want to put the Menu class in Application\View\Helper namespace. Thus, start from creating
the Menu.php file in the View/Helper directory under the Application modules source directory
(figure 6.10).
Why do we place the view helper class under modules source directory?
View helpers (unlike .phtml view templates) are stored under modules src/ directory,
because they are usual PHP classes and require to be resolved by a PHP class autoloading feature. On the other hand, view templates are resolved by the special ZF2 class
called view resolver, and for this reason, view templates are stored under the modules
view/ directory.
<?php
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
// This view helper class displays a menu bar.
class Menu extends AbstractHelper {
// Menu items array.
protected $items = array();
// Active item's ID.
protected $activeItemId = '';
// Constructor.
public function __construct($items=array()) {
17
18
19
20
21
22
23
24
25
26
27
28
29
173
$this->items = $items;
}
// Sets menu items.
public function setItems($items) {
$this->items = $items;
}
// Sets ID of the active items.
public function setActiveItemId($activeItemId) {
$this->activeItemId = $activeItemId;
}
}
In the code above, we defined several private fields for the Menu class. The $items field (line 10)
is an array which will store the information on the menu items; and the $activeItemId field
(line 13) is the ID of an active menu item. The active menu item will be visually highlighted.
In lines 15-18, we defined the class constructor method, which (optionally) takes the array of
items for initializing the menu. An alternative method of menu initialization is through the
setItems() method (lines 20-23). And the setActiveItemId() method (lines 25-28) sets the ID
of the currently active menu item.
Next, lets add the render() method, which will generate HTML code for the whole navigation
bar and return it as a text string:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
174
foreach($this->items as $item) {
$result .= $this->renderItem($item);
}
$result .= '</ul>';
$result .= '</div>';
$result .= '</nav>';
return $result;
}
In the code above, we produce the HTML markup for the Bootstrap navbar component. The
navbar will use the default theme and will be collapsible (adaptive to different screen widths).
The navbar will not have the brand text in the header. In lines 24-26, we loop through the menu
items and render each one with the renderItem() method. Finally, the render() method returns
the resulting HTML code as a text string.
To finish with creating the Menu class, lets implement the renderItem() method. This method
will produce the HTML code for a single menu item:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Renders an item.
protected function renderItem($item) {
$id = isset($item['id']) ? $item['id'] : '';
$isActive = ($id==$this->activeItemId);
$label = isset($item['label']) ? $item['label'] : '';
$result = '';
if(isset($item['dropdown'])) {
$dropdownItems = $item['dropdown'];
$result .= '<li class="dropdown ' . ($isActive?'active':'') . '">';
$result .= '<a href="#" class="dropdown-toggle" data-toggle="dropdown">';
$result .= $label . ' <b class="caret"></b>';
$result .= '<ul class="dropdown-menu">';
foreach($dropdownItems as $item) {
$link = isset($item['link']) ? $item['link'] : '#';
$label = isset($item['label']) ? $item['label'] : '';
$result .= '<li>';
$result .= '<a href="'.$link.'">'.$label.'</a>';
$result .= '</li>';
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
175
}
$result .= '</ul>';
$result .= '</a>';
$result .= '</li>';
} else {
$link = isset($item['link']) ? $item['link'] : '#';
$result .= $isActive?'<li class="active">':'<li>';
$result .= '<a href="'.$link.'">'.$label.'</a>';
$result .= '</li>';
}
return $result;
}
In the renderItem() methods code above we did the following. First, we checked whether the
item is a dropdown menu or a simple item (line 10). If the item is a dropdown menu, we walk
through the dropdown menu items, and render each one in turn (lines 15-20). Lines 25-30 contain
the rendering code for the case of a simple item.
To be able to use the Menu view helper in a view template, it is required to register it in
configuration. To do that, add the following view_helpers key in the module.config.php file:
<?php
return array(
// ...
// The following registers our custom view
// helper classes in view plugin manager.
'view_helpers' => array(
'invokables' => array(
'mainMenu' => 'Application\View\Helper\Menu',
),
),
);
In the example above, we registered our Menu class as a mainMenu view helper and will be able
to access it from any view template.
Since we plan to use the Menu view helper in the layout view template, replace the navigation
menu markup in layout.phtml file with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
176
In the code above, we access the registered mainMenu view helper and set the navigation bar
items with the help of setItems() method (line 1). As a parameter for the method, we pass the
array of items. Then we render the navigation bar with the render() method.
To set the active item for the navigation bar, we can call the setActiveItemId() method from
any view template. For example, add the following code to the beginning of the view template
for the About page (application/index/about.phtml) as follows:
177
<?php
$this->mainMenu()->setActiveItemId('about');
?>
Now, if you open the About page in your browser, you should see that the About item of the
navigation menu is highlighted with a different color. To display the active item properly, you
need to call the setActiveItemId() method for each page presenting in the navbar (Home,
Downloads, Documentation, etc.) You can see how this is done in the Hello World sample.
6.9.2 Breadcrumbs
Now that you know how to implement a view helper, lets create the second view helper for
rendering the breadcrumbs. It is completely analogous to the Menu view helper, so below we just
provide the complete code of the Breadcrumbs class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
namespace Application\View\Helper;
use Zend\View\AbstractHelper;
// This view helper class displays breadcrumbs.
class Breadcrumbs extends \Zend\View\Helper\AbstractHelper {
// Array of items.
private $items = array();
// Constructor.
public function __construct($items=array()) {
$this->items = $items;
}
// Sets the items.
public function setItems($items) {
$this->items = $items;
}
// Renders the breadcrumbs.
public function render() {
if(count($this->items)==0)
return ''; // Do nothing if there are no items.
// Resulting HTML code will be stored in this var
$result = '<ol class="breadcrumb">';
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
178
To be able to use the Breadcrumbs view helper, register it in the module.config.php file as follows:
179
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
return array(
//...
// The following registers our custom view helper classes.
'view_helpers' => array(
'invokables' => array(
'pageBreadcrumbs' => 'Application\View\Helper\Breadcrumbs',
),
),
);
Since we plan to use the Breadcrumbs view helper in the layout view template, replace the
breadcrumbs markup in layout.phtml file with the following code:
<!-- Breadcrumbs -->
<?php echo $this->pageBreadcrumbs()->render(); ?>
In the code above, we access the pageBreadcrumbs() view helper and call it with the render()
method. The echo operator then outputs the HTML code of the breadcrumbs.
Finally, you need to pass the breadcrumbs items for each view template. For example, add the
following lines in the view template for the About page:
<?php
$this->pageBreadcrumbs()->setItems(array(
'Home'=>$this->url('home'),
'About'=>$this->url('about'),
));
?>
Now, if you open the about page, you should see breadcrumbs as in figure 6.11 below. Site users
will easily see what page they are visiting right now and will not get lost.
180
But, actually the ViewModel class is more than just a variable container plus view template name.
In fact, it is closely related to the layout and page composition.
The third big capability of the view model class is that it allows for combining several view
models in a tree-like structure. Each view model in the tree has the associated view template
name and data variables that can be passed to the view template to control the process of
rendering.
This feature is internally used by Zend Framework 2 when combining the layout view template
and the view template associated with the controllers action method. ZF2 internally creates the
view model for the layout template and assigns it with layout/layout view template name.
When your controllers action method returns the ViewModel object, this object is attached as a
child to the layout view model (see figure 6.12 for an example).
181
Method name
Description
addChild()
getChildren()
hasChildren()
clearChildren()
count()
getIterator()
setTerminal()
terminate()
setCaptureTo()
setAppend()
isAppend()
Below, we provide the brief description of the methods presented in the table above.
The addChild(), getChild(), hasChildren() and clearChildren() methods are used for
(respectively) adding a child view model to the parent one, retrieving the array of view models
attached, testing if the view model is leaf (doesnt have children) and detaching all children.
The setCaptureTo() method allows to set the variable in the parent view template into which to
inject the HTML markup code produced by a child view template. If two child view models use
the same variable, the second one will overwrite the first one. The setAppend() method can be
used when you need to inject the results of two or more view templates into a single placeholder
variable. The next rendered view template will be appended to the variables existing content.
The view model returned by the controller is assigned the $content capture variable.
A view model can be marked as terminal with the setTerminal() method. The setTerminal()
method takes a single flag parameter. If true, the view model is considered as terminal (top-level
parent) and the renderer returns the output of the view template to the application, otherwise its
parents are rendered as well. The method terminate() tests whether the view model is terminal
or not.
The setTerminal() method is very useful in some situations, because with its help you
can disable the rendering of the layout view template. If you return from controller the
view model marked as terminal, the layout will not be applied. This can be used, for
example, when you want to load part of a page asynchronously by an AJAX request
and need to insert its HTML code in the DOM tree of an existing page.
6.11 Summary
Zend Framework 2 is shipped with Twitter Bootstrap that is a CSS framework allowing for
creating visual appealing and professionally looking web applications. It provides the base CSS
AJAX (stands for Asynchronous JavaScript and XML) is a capability provided by modern browsers which can be used to send data to, and
retrieve data from, a server asynchronously (in background) without interfering with the display and behavior of the existing page.
182
rules, the simple layout grid, and useful interface components (like navigation bars, breadcrumbs,
pagination, etc.)
In a typical web site, pages have common structure (for example, a typical page may have
a navigation bar at the top, the body with page content, and the footer with the copyright
information at the bottom). In Zend Framework 2, you define this common structure with a
view template file called the layout. The layout template may have placeholder(s) in which ZF2
puts the content specific to a particular web page.
View helpers are (relatively) simple PHP classes that encapsulate a part of page rendering work.
For example, they allow for composing the page of several parts, setting page title and meta tags,
and creating the reusable widgets like navigation bar or breadcrumbs.
Description
Zend\Form
Zend\Filter
Zend\Validator
Zend\InputFilter
To install the example, you can either edit your default virtual host file or create a new
one. After editing the file, restart the Apache HTTP Server and open the web site in
your web browser. For additional information on Apache virtual hosts, you can refer
to Appendix A.
https://github.com/olegkrivtsov/using-zend-framework-2-book
184
The <button> field is analogous to <input type="button">, however it provides additional capabilities, like specifying a graphical icon
on the button.
185
Field
Definition
Text area
<textarea rows=4></textarea>
Password
Button
Submit button
Reset button
Checkbox
Radio
Select
<select><option>Enable</option><option>Disable</option></select>
File
Hidden field
HTML5 introduced several new form field types (listed in table 7.2); figure 7.2 contains
corresponding field visualizations.
HTML5 fields provide more convenient ways for entering the most frequently used data types:
numbers, dates, E-mails, URLs, etc. Additionally, on form submit, the web browser validates that
the user entered data is in a correct format, and if not the browser will prevent form submission
and ask the user to correct the input error.
Table 7.2. HTML5 form fields
Field
Definition
Color picker
Date
Date-time (with time zone)
Date-time (without time zone)
E-mail address
Number
Time
Month
Week
URL
Range (slider)
Search field
Telephone number
<input
<input
<input
<input
<input
<input
<input
<input
<input
<input
<input
<input
<input
type="color" />
type="date" />
type="datetime" />
type="datetime-local" />
type="email" />
type="number" />
type="time" />
type="month" />
type="week" />
type="url" />
type="range" />
type="search" name="googlesearch" />
type="tel" />
186
At the moment of writing this chapter, not all modern web browsers completely support
HTML5 form fields.
7.2.1 Fieldsets
You can group related form fields with the help of the <fieldset> tag, as shown in the example
below. The optional <legend> tag allows you to define the caption for the group.
<fieldset>
<legend>Choose a payment method:</legend>
<input type="radio" name="payment" value="paypal">PayPal</input>
<input type="radio" name="payment" value="card">Credit Card</input>
</fieldset>
The HTML markup presented above will generate the group as in figure 7.3:
187
In the example above, we have the feedback form which allows the user to enter his E-mail
address, message subject, text, and then submit them to the server. The form definition begins
with the <form> tag (line 1).
The <form> tag contains several important attributes:
the name attribute specifies the name of the form (contact-form).
the action attribute defines the URL of the server-side script which is responsible for
processing the submitted form (/contactus).
the method attribute defines the method (either GET or POST) to use for delivering form
data. In this example, we use the POST method (recommended).
In line 3, we define a text input field with the help of the <input> element. The name attribute
specifies the name of the field (email). The type attribute specifies the purpose of the element
(the type text means the input field is intended for entering text).
In line 2, we have the <label> element which represents the label for the E-mail text input field
(the corresponding input fields name is determined by the for attribute of the <label> element).
In lines 5-6, by analogy, we have the Subject input field and its label.
In line 9, we have the text area field which is suited well for entering multi-line text. The height
of the text area (6 rows) is defined by the rows attribute.
In line 11, we have the submit button (input element with submit type). The value attribute
allows you to set the title text for the button (Submit). By clicking this button, the user will
send the form data to the server.
Line break <br> elements are used in lines 4, 7 and 10 to position form controls one below another
(otherwise they would be positioned in one line).
To see what this form looks like, you can put its HTML markup code in a .html file and open
the file in your browser. You will see the form visualization as in figure 7.4.
188
If you enter some data in the feedback form and click the Submit button, the web browser will
send an HTTP request to the URL you specified in the action attribute of the form. The HTTP
request will contain the data you entered.
Above, you can see that the form data is transmitted in request body (line 10). Form fields are
concatenated in a single string and then URL-encoded to replace unsafe characters with allowed
characters from the ASCII table.
In comparison, when you set the GET method for the form, an HTTP request will look like the
example below (the back-slash () character indicate a line break inserted where the line is too
long for a book page):
1
2
3
4
5
6
7
189
GET http://localhost/contactus?email=name%40example.com&\
subject=Example+Subject&body=Hello%21&submit=Submit HTTP/1.1
Host: localhost
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64)
Accept-Encoding: gzip,deflate,sdch
In the example above, you can see that the form data is concatenated, URL-encoded and sent as
part of the HTTP requests URL (line 1), which makes the URL long and harder to read. Since
the form data is sent inside the URL, this makes it easily visible to site visitors.
In most cases, you will use the POST method for delivering form data in the request body, because
the user doesnt need to see the data in the browsers navigation bar (especially when submitting
passwords or other sensitive data).
Please note that submitting form data using the POST method does not protect your
sensitive data (like passwords, credit card numbers, etc.) from being stolen. To protect
such data, youll have to direct your HTTP traffic to a SSL tunnel (SSL stands for
Secure Sockets Layer). Protected SSL connections are distinguished by using the https://
schema in web page URLs. To enable SSL for your Apache HTTP server, you will need
to obtain an SSL certificate from a trusted provider (like VeriSign) and install it on
your server.
http://en.wikipedia.org/wiki/Secure_Sockets_Layer
http://www.verisign.com/
190
<h1>Contact Us</h1>
<p>
Please fill out the following form to contact us.
We appreciate your feedback.
</p>
<form name="contact-form" action="/contactus" method="post">
<div class="form-group">
<label for="email">Your E-mail</label>
<input name="email" type="text" class="form-control"
placeholder="[email protected]">
</div>
<div class="form-group">
<label for="subject">Subject</label>
<input name="subject" type="text" class="form-control"
placeholder="Type subject here">
</div>
<div class="form-group">
<label for="body">Message Body</label>
<textarea name="body" class="form-control" rows="6"
placeholder="Type message text here"></textarea>
</div>
<input name="submit" type="submit"
class="btn btn-primary" value="Submit">
</form>
191
In the HTML markup above, we put a form inside of the 6-column-width grid cell, which makes
the form half the width of the screen.
192
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
namespace Application\Controller;
// ...
class IndexController extends AbstractActionController {
// This action displays the feedback form
public function contactUsAction() {
// Check if user has submitted the form
if($this->getRequest()->isPost()) {
// Retrieve form data from POST variables
$data = $this->params()->fromPost();
// ... Do something with the data ...
var_dump($data);
}
// Pass form variable to view
return new ViewModel(array(
'form' => $form
));
}
}
In the code above, we define the contactUsAction() action method in the IndexController
class (line 9).
Then, in line 12, we check whether the request is a POST request (checking the starting line of
the HTTP request). Typically, the form uses the POST method for submitting the data. For this
193
reason, we can detect if the form is submitted or not by checking the starting line of the HTTP
request.
In line 15 we retrieve the raw data submitted by the user. We extract all the POST variables with
the help of the Params controller plugin. The data is returned in the form of an array and saved
into the $data variable.
Finally, we have to add a literal route to make a short and memorable URL for the Contact Us
page. Add the following contactus key to the routing configuration in the module.config.php
file:
<?php
return array(
// ...
'router' => array(
'routes' => array(
// Add the following routing rule for the "Contact Us" page
'contactus' => array(
'type' => 'Zend\Mvc\Router\Http\Literal',
'options' => array(
'route'
=> '/contactus',
'defaults' => array(
'controller' => 'Application\Controller\Index',
'action'
=> 'contactUs',
),
),
),
),
),
),
// ...
);
Now, if you type the http://localhost/contactus URL in your web browsers navigation bar,
you should see the page as in figure 7.7. Enter an E-mail, subject, and body text and click the
Submit button on the form. The data will be sent to the server, and finally extracted in the
IndexController::contactUsAction() method.
Below, an example of the $data array (produced with the var_dump() PHP function) is shown.
As you can see, the array contains a key for each form field, including the submit field.
194
array (size=4)
'email' => string '[email protected]' (length=16)
'subject' => string 'Happy New Year!' (length=15)
'body' => string 'Dear Support, I'd like to thank you for the
excellent quality of your support service and wish you
a happy new year!' (length=118)
'submit' => string 'Submit' (length=6)
195
has a disadvantage in that we do not check user-submitted data for possible errors and/or
malicious code. Here we will discuss how to perform such validation.
In a ZF2-based web site that uses the Model-View-Controller pattern, form functionality is
usually separated into form models responsible for field definition, filtering and validation; and
form presentation (view) which is typically implemented with the help of special view helpers.
The functionality allowing to create form models, add validation rules and use view helpers, is
schematically shown in figure 7.8. As you can see from the figure, the standard HTML forms
functionality is used as a base.
The MVC approach to working with forms has the following advantages:
You are able to reuse your form model in different controllers actions.
By using the view helpers, you can avoid the boring work of preparing HTML markup for
rendering the form and its possible validation errors.
You are able to create one or several visual representations for the same form model.
By encapsulating the form validation logic in a single form model class you have fewer
places in your code where you need to check user input, thus you improve your site
security.
196
the form model and pass it the user-submitted data. The form models work is to check
(validate) the data for correctness, and if something is wrong, produce error message(s) for
any invalid form field.
2. Secondly, you pass the form model to the .phtml view template for rendering (with the
help of the ViewModel variable container). The view template then will be able to access
the form model and call its methods.
3. And finally, the view template uses the form model and the view helpers provided by Zend
Framework 2 to render the form fields (and to display possible validation error messages
produced at the validation stage). As a result, the HTML markup of the form is produced.
In the following sections, we will discuss these in more detail.
197
can see from the figure, the Form class extends the Fieldset class. The Fieldset class, in turn,
is derived from the Element class which represents a single form field and its attributes.
This class inheritance may look strange at first sight, but everything becomes logical
if you remember that the Form class inherits methods for adding form fields from the
Fieldset class, and that it inherits methods for setting form attributes from the Element
class.
Below, we provide a stub model class for the feedback form from our previous examples:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
namespace Application\Form;
use Zend\Form\Form;
// A feedback form model
class ContactForm extends Form
{
// Constructor.
public function __construct()
{
// Define form name
parent::__construct('contact-form');
// Set POST method for this form
$this->setAttribute('method', 'post');
18
19
20
21
22
23
198
As you can see, form models of the web sites Application module (by convention) belong to
Application\Form namespace (line 2).
In line 7, we define the ContactForm form model class which extends the Form base class.
In line 10, we define the constructor method for the class. Because we derive our form model
from the base Form class, we have to call the parent class constructor to initialize it (line 13).
The parent class constructor accepts an optional argument allowing it to set the forms name
(contact-form).
We can also set form data delivery method (POST) by using the setAttribute() method
provided by the base class (line 16). The setAttribute() takes two parameters: the first one
is the name of the attribute to set, and the second one is the value of the attribute.
You also can set the forms action attribute (line 19) with the setAttribute() method,
analogous to the way you did with the name attribute. Actually, as you will see later, setting
the forms action attribute is optional.
Setting the action attribute for the form is optional, because empty form action forces
the browser to submit form data to the URL of the current page. This is sufficient in
most scenarios, because usually you use the single controller action for both displaying
the form and processing its data.
Form fields are typically created inside of the form models constructor (look at line 21). In the
next section, we will learn which form fields are available and how to add them to the form
model.
199
Concrete form element classes extend the Element base class. They are listed in table 7.3. These
classes live in the Zend\Form\Element namespace.
Table 7.3. Form elements
Class name
Description
Button.
Check box.
File field.
Hidden field.
Image field.
Password field.
Radio button.
Dropdown list.
Submit button.
General-purpose text input field.
Multi-line text area.
HTML5 Elements
Color
Date
DateTime
DateTimeLocal
Email
Month
Number
Time
Url
Week
Range
Color picker.
Date picker.
Date & time (with time zone).
Date & time (without time zone).
E-mail field.
Month input field.
A text input field accepting numbers.
Text input field for entering time.
Text input field for entering an URL.
Text input field for entering days of week.
Range field (slider).
Compound Fields
MultiCheckbox
DateTimeSelect
DateSelect
MonthSelect
200
Class name
Description
Captcha
Csrf
Other
Collection
Element collection.
In the table above, you can see that the ZF2-provided form elements have direct mapping on
HTML4 and HTML5 input fields (discussed in the beginning of this chapter).
For your convenience, ZF2 also provides several compound fields. The MultiCheckbox field
is a field which is composed of a group of typical checkboxes related to each other. The
DateTimeSelect, DateSelect, and MonthSelect elements are analogous to corresponding HTML5
elements, but simulate them with the usual select fields. These input fields have an advantage in
that they are supported by all web browsers, unlike the corresponding HTML5 fields. The visual
representation of these elements can be seen in figure 7.11.
Additionally, ZF2 provides security form fields Captcha and Csrf, which can be used on a form
for enhancing the security. The Captcha element is a graphical element (image) that is placed
on a form for checking if the site user is a human or a robot. The Csrf element has no visual
representation and is used for prevention of hacker attacks related to cross-site request forgery
.
There is another special form element called Collection. This element is analogous to fieldset,
because it allows you to group related form elements. But, it is designed for adding form elements
dynamically by binding an array of objects to the form.
Cross-site request forgery (CSRF) is a type of malicious exploit of a website whereby unauthorized commands are transmitted from a user
that the website trusts.
201
Method name
Description
add($elementOrFieldset, $flags)
has($elementOrFieldset)
get($elementOrFieldset)
getElements()
getFieldsets()
count()
remove($elementOrFieldset)
Particularly, we are interested in the add() method which is used to attach an element to a form.
The add() method takes two arguments: the first one (named $elementOrFieldset) is an element
to insert, and the second one (named $flags) is the optional flags.
The $elementOrFieldset parameter may either be an instance of an Element-derived class (or
the Fieldset class), or an array describing the element that should be created.
The optional $flags argument is an array which may contain a combination of the following
keys: name (allows you to set the elements name) and priority (allows to specify the zerobased index in the list of elements to insert the element to. If the priority flag is not specified,
the element will be inserted at the end of the list of the form models elements.
Below, we provide two code examples illustrating the possible ways of adding elements to a
form.
<?php
namespace Application\Form;
// Define an alias for the class name
use Zend\Form\Form;
use Zend\Form\Element\Text;
// A feedback form model
class ContactForm extends Form
{
// Constructor.
public function __construct()
202
13
14
15
16
17
18
19
20
21
22
23
24
25
{
// Create the form fields here ...
$element = new Text(
'subject',
// Name of the element
array(
// Array of options
'label'=> 'Subject' // Text label
));
$element->setAttribute('id', 'subject');
// Add the "subject" field to the form
$this->add($element);
}
}
In the code above, we created an instance of the Zend\Form\Element\Text class (line 15). The
class constructor takes two parameters: the elements name (subject) and an array of options
(here we specify the text label Subject).
Additionally, you may configure the element using the methods provided by the Element base
class. For example, in line 20, we set the id attribute with the setAttribute() method. For
your reference, the (most important) methods of the Element base class which can be used for
configuring a form element are presented in table 7.5.
Table 7.5. Methods provided by the Element class
Method name
Description
setName($name)
getName()
setOptions($options)
getOptions($options)
getOption($option)
setAttribute($key, $value)
getAttribute($key)
removeAttribute($key)
hasAttribute($key)
setAttributes($arrayOrTraversable)
getAttributes()
clearAttributes()
setValue()
getValue()
setLabel()
getLabel()
setLabelAttributes()
getLabelAttributes()
setLabelOptions()
getLabelOptions()
203
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
namespace Application\Form;
// Define an alias for the class name
use Zend\Form\Form;
// A feedback form model
class ContactForm extends Form
{
// Constructor.
public function __construct()
{
// Add "subject" field
$this->add(array(
'type' => 'text',
'name' => 'subject',
'attributes' => array(
'id' => 'subject',
),
'options' => array(
'label' => 'Subject',
),
));
}
}
// Element type
// Field name
// Array of attributes
// Array of options
// Text label
204
In line 14 above, we call the form models add() method to add the element to form. We pass the
element specification to the add() method in the form of an array. The array has the following
typical keys:
the type key (line 15) defines the class name to use for instantiation of the element. Here
you can use either the full class name (e.g. Zend\Form\Element\Text) or its short alias
(e.g. text).
the name key (line 16) defines the name for the field (subject).
the attributes key (line 17) defines the list of HTML attributes to set (here we set the id
attribute).
the options array (line 18) allows you to specify the text label for the element.
you are confused where we take element aliases from, than you should know that they are defined inside of the
Zend\Form\FormElementManager class.
205
We put the field creation logic into the addElements() protected method to better
structure the form models code.
<?php
namespace Application\Form;
use Zend\Form\Form;
/**
* This form is used to collect user feedback data like user E-mail,
* message subject and text.
*/
class ContactForm extends Form
{
// Constructor.
public function __construct()
{
// Define form name
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
parent::__construct('contact-form');
// Set POST method for this form
$this->setAttribute('method', 'post');
// Add form elements
$this->addElements();
}
// This method adds elements to form (input fields and
// submit button).
private function addElements() {
// Add "email" field
$this->add(array(
'type' => 'text',
'name' => 'email',
'attributes' => array(
'id' => 'body'
),
'options' => array(
'label' => 'Your E-mail',
),
));
// Add "subject" field
$this->add(array(
'type' => 'text',
'name' => 'subject',
'attributes' => array(
'id' => 'subject'
),
'options' => array(
'label' => 'Subject',
),
));
// Add "body" field
$this->add(array(
'type' => 'text',
'name' => 'body',
'attributes' => array(
'id' => 'body'
),
'options' => array(
'label' => 'Message Body',
206
62
63
64
65
66
67
68
69
70
71
72
73
74
207
),
));
// Add the submit button
$this->add(array(
'type' => 'submit',
'name' => 'submit',
'attributes' => array(
'value' => 'Submit',
),
));
}
}
In line 10 above, we define the ContactForm class which extends the Form base class.
In lines 13-23, we have the constructor method. It calls the base class constructor (line 16) and
passes the form name as its argument (contact-form). In line 19, the base class setAttribute()
method is called allowing you to set the method name for the form (we set the POST method).
In line 22, the addElements() protected method is called, which does the actual work of adding
elements to the form. The code of the addElements() is located in lines 27-70. To add elements
to the form, we call the add() method provided by the base class. This method accepts the single
argument an array containing configuration for an element. We add four fields: the email, the
subject, the body and the submit field.
In figure 7.14, you can see schematic graphical representation of the form model we have created.
208
209
An input may consist of filters and/or validators and some additional information. For
example, an input may contain the flag telling if the field is required or if its value may
be missing from HTTP request.
Analogous to adding a form models fields, there are two possible ways of adding inputs to the
input filter container: either via passing an instance of an input class as the argument of its add()
method, or via passing the array specification . In the next section, we will describe the latter
method (it is preferable, because it requires less code to write).
array(
'name'
=> '<name>',
'type'
=> '<type>',
'required' => <required>,
'filters' => array(
// Add filters configuration here ...
),
'validators' => array(
// Add validators configuration here ...
)
)
210
array(
'name' => '<filter_name>',
'priority' => <priority>
'options' => array(
// Filter options go here ...
)
),
The name key (line 2) is the name for the filter. This may be either a full filter class name (e.g.
Zend\Filter\StringTrim or an alias (e.g. StringTrim).
The optional priority key (line 3) defines filter priority in the list of filters. The priority must
be an integer number. The filters with the highest priority will be applied first. By default, the
FilterChain::DEFAULT_PRIORITY constant (value 1000) is assigned.
The options array (line 4) is specific to a certain filter and may contain parameters for
configuring the filter.
7.9.2.2 Validator Configuration
A typical validator configuration is presented below:
211
1
2
3
4
5
6
7
array(
'name' => '<validator_name>',
'break_chain_on_failure' => <flag>
'options' => array(
// Validator options go here ...
)
),
The name key (line 2) is the name for the validator. This may be either a full validator class name
(e.g. Zend\Validator\EmailAddress or an alias (e.g. EmailAddress).
The break_chain_on_failure optional key (line 3) defines the behavior in case the validator
check fails. If this equals to true, subsequent validators in the list will not be executed; otherwise
every validator in the list will be executed without depending on the result of other validators.
The options array (line 4) is specific to certain validator class and may contain parameters for
configuring the validator.
7.9.2.3 Attaching Input Filter to Form Model
Once you have created and populated the input filter container, you have to attach it to the form
model. The Form base class provides the setInputFilter() method intended for this purpose
(see table 7.6).
Table 7.6. Methods provided by the Form base class
Method name
Description
setInputFilter($inputFilter)
getInputFilter()
<?php
// ...
use Zend\InputFilter\InputFilter;
class ContactForm extends Form
{
public function __construct()
{
// ... call this method to add validation rules
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
$this->addInputFilter();
}
// ...
// This method creates input filter (used for form filtering/validation).
private function addInputFilter() {
$inputFilter = new InputFilter();
$this->setInputFilter($inputFilter);
$inputFilter->add(array(
'name'
=> 'email',
'required' => true,
'filters' => array(
array('name' => 'StringTrim'),
),
'validators' => array(
array(
'name' => 'EmailAddress',
'options' => array(
'allow' => \Zend\Validator\Hostname::ALLOW_DNS,
'useMxCheck' => false,
),
),
),
)
);
$inputFilter->add(array(
'name'
=> 'subject',
'required' => true,
'filters' => array(
array('name' => 'StringTrim'),
array('name' => 'StripTags'),
array('name' => 'StripNewLines'),
),
'validators' => array(
array(
'name' => 'StringLength',
'options' => array(
'min' => 1,
'max' => 128
),
),
),
212
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
213
)
);
$inputFilter->add(array(
'name'
=> 'body',
'required' => true,
'filters' => array(
array('name' => 'StripTags'),
),
'validators' => array(
array(
'name' => 'StringLength',
'options' => array(
'min' => 1,
'max' => 4096
),
),
),
)
);
}
}
As you can see from the code above, first we declare the alias for the Zend\InputFilter\InputFilter
class (line 3).
In the form models constructor (line 10), we call the addInputFilter() method which we define
in lines 16-76.
The addInputFilter() methods goal is to create the InputFilter container (line 18), attach it
to form model (line 19) and add filtering/ validation rules (lines 21-75). For attaching the input
filter to the form model, we use the setInputFilter() method provided by the Form base class.
For inserting filtering/validation rules into the input filter container, we use the add() method
provided by the InputFilter class, which takes the array specification of an input to create.
We add three inputs (per each field of our form model, except its submit button):
For the email field, we set the required flag to true to make filling this field mandatory.
We use StringTrim filter to remove white spaces from the beginning and the end of the Email address; and the EmailAddress validator for checking the user-entered E-mail address
for correctness. We configure the EmailAddress validator to allow domain names as Email addresses (the \Zend\Validator\Hostname::ALLOW_DNS flag) and disable MX record
checking (set useMxCheck option to false).
For the subject field, by analogy, we make it required, and use the StringTrim filter
to remove white spaces from the beginning and the end. Additionally, we use the
StripNewLines and StripTags filters to filter out the new line characters and HTML tags,
respectively. We constrain subject string length to be between 1 and 128 characters in
length by using the StringLength validator.
214
For the body field, we require it to be mandatory, and we use the StripTags filter to strip
HTML tags from E-mail text. We also use the StringLength validator to constrain E-mail
text to be between 1 and 4096 characters in length.
In figure 7.16, you can find the schematic graphical representation of the input filter weve
created.
Above, we briefly described how to create an input filter for the form model. For
detailed information about the above mentioned (and other) filters and validators and
their usage examples, please refer to Chapter 8.
215
First, you display the form and its fields on a web page, prompting user for input. Once
the user fills the form fields, he clicks the Submit button and sends the data to server.
Next, your controller extracts the submitted data and asks the form model to validate it. If
there were input errors, you display the form again, asking the user to correct input errors.
If the data is correct, you process the data with your business logic layer and (usually)
redirect the user to another web page.
The Form base class provides several methods for accomplishing these (see table 7.7).
216
Method name
Description
setData($data)
getData($flag)
isValid()
hasValidated()
getMessages($elementName = null)
<?php
namespace Application\Controller;
use Application\Form\ContactForm;
// ...
class IndexController extends AbstractActionController {
// This action displays the feedback form
public function contactUsAction() {
// Create Contact Us form
$form = new ContactForm();
// Check if user has submitted the form
if($this->getRequest()->isPost()) {
// Fill in the form with POST data
$data = $this->params()->fromPost();
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
217
$form->setData($data);
// Validate form
if($form->isValid()) {
// Get filtered and validated data
$data = $form->getData();
// ... Do something with the validated data ...
// Redirect to "Thank You" page
return $this->redirect()->toRoute('application/default',
array('controller'=>'index', 'action'=>'thankYou'));
}
}
// Pass form variable to view
return new ViewModel(array(
'form' => $form
));
}
}
In the code above, we define the contactUsAction() action method in the IndexController
class (line 10). In the action method, we create an instance of the ContactForm class (line 13).
Then, in line 16, we check whether the request is a POST request (checking the starting line of
HTTP request).
In line 19 we retrieve the raw data submitted by the user. We extract all the POST variables with
the help of the Params controller plugin. The data is returned in the form of an array and saved
into the $data variable.
The data submitted by the user may contain mistakes and should be filtered and validated before
further usage. To do that, in line 20 we set the data to the form model with the setData()
method provided by the Form base class. We validate form data with the isValid() method (line
23), which returns true upon successful validation. If the validation succeeds, we retrieve the
validated data using the getData() method (line 26) and then can pass the data to our business
logic layer.
Once we have used the validated data, in line 31, we redirect the web user to the Thank You page.
The redirect is performed with the Redirect controller plugin. The Redirect plugins toRoute()
method takes two parameters: the first parameter is the name of the route (application/default),
and the second one is the array of parameters to pass to the router. These identify the web page
where you redirect the user.
We will prepare the controllers action and view template for the Thank You page a
little bit later.
218
In line 37, we pass the form model through the $form variable to the view template. The view
template will access this variable and will use it for rendering the form (and possible validation
errors).
The MailSender model will internally use the Zend\Mail component. The Zend\Mail component is a component provided by Zend Framework 2 and designed to give you the convenient functionality for composing mail messages (the Zend\Mail\Message class) and several
classes implementing available transports for sending mail (in this example, we will use the
Mail\Transport\Sendmail class which uses the sendmail program for delivering E-mails).
The sendmail program is a free open-source mail transfer agent for Linux/Unix
operating systems. It accepts messages that a PHP script passes to it, deciding based
upon the message header which delivery method it should use, and then passes the
message through the SMTP protocol to the appropriate mail server (like Google Mail)
for delivery to the recipient.
Start with creating the MailSender.php file under the Service directory under the modules source
directory (see figure 7.18 for example).
The following is the code that should be put into the MailSender.php file:
In DDD terms, the MailSender can be related to service models, because its goal is to manipulate data, not to store data.
http://www.sendmail.com/sm/open_source/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
219
<?php
namespace Application\Service;
use Zend\Mail;
use Zend\Mail\Message;
use Zend\Mail\Transport\Sendmail;
// This class is used to deliver an E-mail message to recipient.
class MailSender {
// Sends the mail message.
public function sendMail($sender, $recipient, $subject, $text) {
$result = false;
try {
// Create E-mail message
$mail = new Message();
$mail->setFrom($sender);
$mail->addTo($recipient);
$mail->setSubject($subject);
$mail->setBody($text);
// Send E-mail message
$transport = new Sendmail('-f'.$sender);
$transport->send($mail);
$result = true;
} catch(\Exception $e) {
$result = false;
}
// Return status
return $result;
}
}
In the code above, we define the Application\Service namespace (line 2), because the
MailSender class can be related to service models (its goal is to manipulate data, not to store
it).
In lines 4-6, we declare the aliases for the Mail, Message and Transport\Sendmail classes
provided by the Zend\Mail component.
In lines 9-35, we define the MailSender class. The class has the single method sendMail() (line
12), which takes four arguments: senders E-mail address, recipients E-mail address, message
subject and, finally, message body text.
220
In line 18, we create an instance of the Message class. We use the methods provided by this class
for composing the message (set its subject, body etc.) in lines 19-22.
In line 25, we create an instance of the Sendmail class, which uses the sendmail program to pass
the message to the appropriate mail server (see lines 25-26). Since the classes provided by the
Zend\Mail component may throw an exception on failure, we enclose the block of code with the
try-catch exception handler.
The sendMail() method will return true if the E-mail message sent successfully; otherwise it
will return false (line 33).
Configuring mail system for your web server is a rather complex task. It typically
requires installing sendmail and configuring the servers MX DNS record to use certain
mail server (either local mail server, e.g. Posftix, or remote server, like Google Mail).
Because of the complexity of the topic, it is not discussed in this book. You can find
additional information on configuring mail for your particular system online.
<?php
// ...
use Application\Service\MailSender;
class IndexController extends AbstractActionController {
// ...
public function contactUsAction() {
// Create Contact Us form
$form = new ContactForm();
// Check if user has submitted the form
if($this->getRequest()->isPost()) {
// Fill in the form with POST data
$data = $this->params()->fromPost();
$form->setData($data);
// Validate form
if($form->isValid()) {
// Get filtered and validated data
http://www.postfix.org/
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
221
$data = $form->getData();
$email = $data['email'];
$subject = $data['subject'];
$body = $data['body'];
// Send E-mail
$mailSender = new MailSender();
if(!$mailSender->sendMail(
'[email protected]', $email, $subject, $body)) {
// In case of error, redirect to "Error Sending Email" page
return $this->redirect()->toRoute('application/default',
array('controller'=>'index', 'action'=>'sendError'));
}
// Redirect to "Thank You" page
return $this->redirect()->toRoute('application/default',
array('controller'=>'index', 'action'=>'thankYou'));
}
}
// Pass form variable to view
return new ViewModel(array(
'form' => $form
));
}
// This action displays the Thank You page. The user is redirected to this
// page on successful mail delivery.
public function thankYouAction() {
return new ViewModel();
}
// This action displays the Send Error page. The user is redirected to this
// page on mail delivery error.
public function sendErrorAction() {
return new ViewModel();
}
}
222
In line 31, we instantiate the MailSender class with the new operator; in line 32, we call its
sendMail() method and pass it four parameters: the senders address (here we use [email protected], but you can replace this with the address of your sendmail); the
recipients E-mail address, the E-mail subject and body.
If mail has been sent successfully (if the sendMail() method returned true), we redirect
the user to the Thank You page (line 40). On failure (if sendMail() method returned false),
we redirect the user to the Send Error page (line 35).
In lines 53-55, we have the thankYouAction() method which displays the Thank You page.
This page is shown if the E-mail message is sent successfully.
In line 59-61, we have the sendErrorAction() method which shows the Error Sending
Email page. This page is shown on E-mail delivery failure.
Method name
Description
prepare()
223
Method name
Description
Generic helpers
Form
FormElement
FormElementErrors
FormRow
224
Method name
Description
FormDateTime
FormDateTimeLocal
FormEmail
FormMonth
FormNumber
FormRange
FormTel
FormTime
FormUrl
FormWeek
Other helpers
FormCaptcha
FormDateSelect
FormDateTimeSelect
FormMonthSelect
FormMultiCheckbox
FormCollection
In the next sections, we will provide an overview of several frequently used form view helpers
and their usage examples.
Method name
Description
render($element)
__invoke($element)
As you can see, there are two methods doing the same thing:
The render() method produces the HTML markup for the form field. It accepts the single
argument the instance of the element to render. You can retrieve the form element with
the form models get() method (see example below).
The __invoke() method is a convenience wrapper which results in less code to write.
225
<?php
// We assume that the form model is stored in $form variable.
// Render the E-mail field with the render() method.
echo $this->formElement()->render($form->get('email')); ?>
// The same, but with __invoke
echo $this->formElement($form->get('email'));
When executed, the code above will generate the HTML code as follows:
<input type="text" name="email" id="email" value="">
Typically, there is no need to call view helpers for concrete HTML (or HTML5) fields
(e.g. FormText, FormSubmit, etc.) Instead, you can use the generic FormElement view
helper which determines the field type automatically and produces the needed HTML
code.
If there were any validation errors, this code will generate the unordered list of errors using the
<ul> HTML tag, and the list will contain as many items as there are errors for certain field. An
example of such list for the E-mail field of our feedback form is presented below:
<ul>
<li>'hostname' is not a valid hostname for the email address</li>
<li>The input does not match the expected structure for a DNS hostname</li>
<li>The input appears to be a local network name but local network names
are not allowed</li>
</ul>
226
<?php
// We assume that the form model is stored in $form variable.
// Render text label for the E-mail field.
echo $this->formLabel($form->get('email'));
When executed, the code above will generate the HTML code as follows:
<label for="email">Your E-mail</label>
Method name
Description
render($element)
__invoke($element, $labelPosition,
$renderErrors, $partial)
setInputErrorClass($inputErrorClass)
setLabelAttributes($labelAttributes)
setLabelPosition($labelPosition)
setRenderErrors($renderErrors)
setPartial($partial)
When executed, the code above will generate the HTML code as follows:
227
Method name
Description
render($form)
__invoke($form)
PHP magic method which renders the entire form and all its
elements (the effect is the same as render()).
openTag($form)
closeTag()
You can render the whole form with the help of the Forms render() method as follows:
// We assume that the form model is stored in $form variable
// Render the whole form
$this->form()->render($form);
The same effect can be achieved with the __invoke magic method (see example below):
// The same, but with `__invoke`
$this->form($form);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
228
<?php
$form = $this->form;
$form->prepare();
?>
<?php echo $this->form()->openTag($form); ?>
<?php echo $this->formLabel($form->get('email')); ?>
<?php echo $this->formElement($form->get('email')); ?>
<?php echo $this->formElementErrors($form->get('email')); ?>
<?php echo $this->formLabel($form->get('subject')); ?>
<?php echo $this->formElement($form->get('subject')); ?>
<?php echo $this->formElementErrors($form->get('subject')); ?>
<?php echo $this->formLabel($form->get('body')); ?>
<?php echo $this->formElement($form->get('body')); ?>
<?php echo $this->formElementErrors($form->get('body')); ?>
<?php echo $this->formElement($form->get('submit')); ?>
<?php echo $this->form()->closeTag(); ?>
As you can see from the code above, we do the following things to render the form:
In line 2, we access the $form variable passed from the controllers action.
In line 3, we call the Forms prepare() method to prepare the form for rendering. Please
note that calling this method is very important. If you forget to do that, there may be some
undesired rendering problems.
In line 6, we call the openTag() method of the Form view helper. Its purpose is to render the
opening <form> tag and its attributes. The method takes a single argument an instance
of the form model. Paired closing </form> tag is rendered in line 23 with the help of the
closeTag() method of the Form view helper.
In lines 8-10, we render the E-mail fields label, the text field itself and (possible) validation
errors with the help of the FormLabel, FormElement and FormElementErrors view helpers.
Those helpers take the instance of the form models element as a single argument. We get
an instance of the element with the get() method provided by the Form base class.
In lines 12-14, by analogy, we render the Subject field, its label and validation errors.
And in lines 16-18, we render the label, the field and the validation errors for the body text
area field.
In line 20, we render the Submit button.
When the view template renderer evaluates this code, it will produce the HTML output like
below:
229
If certain fields have validation errors, those errors will be outputted below the field in the form
of the <ul> unordered HTML list. For example, if you enter the 123@hostname into E-mail
form field, you would receive the following validation errors:
<label for="email">Your E-mail</label>
<input type="text" name="email" value="123@hostname">
<ul>
<li>'hostname' is not a valid hostname for the email address</li>
<li>The input does not match the expected structure for a DNS hostname</li>
<li>The input appears to be a local network name but local network names
are not allowed</li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
$form = $this->form;
$form->prepare();
$form->get('email')->setAttributes(array(
'class'=>'form-control',
'placeholder'=>'[email protected]'
));
$form->get('subject')->setAttributes(array(
'class'=>'form-control',
'placeholder'=>'Type subject here'
));
$form->get('body')->setAttributes(array(
'class'=>'form-control',
'rows'=>6,
'placeholder'=>'Type message text here'
));
$form->get('submit')->setAttributes(array('class'=>'btn btn-primary'));
?>
<h1>Contact Us</h1>
<p>
Please fill out the following form to contact us.
We appreciate your feedback.
</p>
<div class="row">
<div class="col-md-6">
<?php echo $this->form()->openTag($form); ?>
<div class="form-group">
<?php echo $this->formLabel($form->get('email')); ?>
<?php echo $this->formElement($form->get('email')); ?>
<?php echo $this->formElementErrors($form->get('email')); ?>
</div>
<div class="form-group">
<?php echo $this->formLabel($form->get('subject')); ?>
<?php echo $this->formElement($form->get('subject')); ?>
<?php echo $this->formElementErrors($form->get('subject')); ?>
</div>
230
47
48
49
50
51
52
53
54
55
56
57
231
<div class="form-group">
<?php echo $this->formLabel($form->get('body')); ?>
<?php echo $this->formElement($form->get('body')); ?>
<?php echo $this->formElementErrors($form->get('body')); ?>
</div>
<?php echo $this->formElement($form->get('submit')); ?>
<?php echo $this->form()->closeTag(); ?>
</div>
</div>
In the code above, we added the .form-control CSS class to every input field in the form. We did
that with the setAttribute() method (see lines 5, 10 and 15). With that method, we also added
the placeholder attribute to define the nice-looking placeholder text when a field is empty. For
the body field, we added the rows attribute defining the height of the field (6 rows).
For the forms Submit button, we use the .btn and .btn-primary CSS classes (see line 21).
We also put label-input pairs inside of <div> elements with .form-group CSS class (lines 35, 41,
47).
We put a form inside of the 6-column-width grid cell, which makes the form half the width of
the screen (look at lines 31-32).
The CSS rules above will remove bullets from the list and make validation error messages appear
in red.
232
7.13.3 Adding the Thank You & Error Sending Email Pages
The last small thing we will do is preparing the view templates for the Thank You and Error
Sending Email pages.
Add the thank-you.phtml view template in application/index/ directory under the modules
view/ directory. Put the following HTML markup into the view template file:
<h1>Thank You!</h1>
<p>
<div class="alert alert-success">
We will respond to the E-mail address you have provided.
</div>
</p>
Next, add the send-error.phtml view template file. The HTML markup for the Error Sending
Email page is presented below:
<h1>Error Sending Email!</h1>
<p>
<div class="alert alert-warning">
Sorry, but we had an unexpected problem when trying to deliver
your message. Please try again later.
</div>
</p>
7.13.4 Results
Congratulations! Now, if you open the http://localhost/contactus URL in your web browser,
you should see a page like that shown in figure 7.19.
If you enter some invalid data in the form and click the Submit button, you should see the
validation errors (figure 7.20).
Entering the correct E-mail, subject and message text and submitting the form results in sending
the message and displaying the Thank You page (see figure 7.21).
On a sending failure, you will see the Error Sending Email page (see figure 7.22 for example):
You can see the Contact Us form in action in the Form Demo sample application bundled
with this book.
233
234
235
7.14 Summary
Forms are the way of collecting user-entered data on web pages. A form usually consists of
elements (input field + label pairs). Elements can optionally be grouped into fieldsets.
In a MVC-based web site, form functionality is separated into form models responsible for
element definition and validation, and form presentation implemented with the help of special
view helpers.
To create a form model, you write a class deriving from the Form base class. The form model is
initialized by adding its elements with the help of the base class-provided methods.
To submit form data to the server, the user clicks the Submit button, then the data is sent as
part of a HTTP request. Once the user submits the form, you can extract the form data in your
controller and ask the form model to validate it.
For checking and filtering the user-entered data, filters and validators are utilized. You use the
InputFilter class which is the container for validation rules.
If there are input errors, you display the form again, asking the user to correct the input errors.
If the data is correct, you process the data with your business logic layer.
Description
Zend\Filter
Zend\InputFilter
8.1.1 FilterInterface
Technically, a filter is a PHP class implementing the FilterInterface interface (it belongs to
Zend\Filter namespace). The interface definition is presented below:
1
2
3
4
5
6
7
8
<?php
namespace Zend\Filter;
interface FilterInterface
{
// Returns the result of filtering $value.
public function filter($value);
}
As you can see, the FilterInterface interface has the single method filter() (line 7), which
takes the single parameter $value. The method transforms the input data and finally returns the
resulting (filtered) value.
237
A concrete filter class implementing the FilterInterface interface may have additional methods. For example, many filter classes have methods allowing configuration
of the filter (set filtering options).
Standard filters provided by the Zend\Filter component, along with a brief description of each,
are listed in table 8.1.
As you can see from the table, the standard filters can be roughly divided into the following
groups:
filters casting input data to a specified type (integer, boolean, date-time, etc.);
filters performing manipulations on a file path (getting the base name, parent directory
name, etc.);
filters performing compression and encryption of input data;
filters manipulating string data (case conversion, trimming, character replacement and
removal, URL normalizing, etc.); and
proxy filters wrapping other filters (Callback, FilterChain and StaticFilter).
In this section, we only consider the standard filters belonging to the Zend\Filter namespace, although there are other filters that can also
be considered standard. For example, the Zend\Filter\File namespace contains several filters applicable to processing file uploads (those filters
will be covered in the next chapter). Additionally, the Zend\I18n component defines several filter classes that are aware of the users locale.
From figure 8.1, you may also notice that there are several more base filters: AbstractUnicode filter is the base class for the StringToUpper
and StringToLower filters, because it provides the string conversion functionality common to both of them. And, the Decompress filter inherits
from the Compress filter, because these filters are in fact very similar. By analogy, the Decrypt filter inherits from the Encrypt filter, because
they are the mirror reflection of each other as well.
238
Class name
Description
Boolean
Int
Digits
Null
Returns null if the input value can be treated as null; otherwise returns the
$value itself.
Takes a date & time string in an arbitrary format and produces a date & time
string in a given format.
DateTimeFormatter
BaseName
Given a string containing the path to a file or directory, this filter will return
the trailing name component.
Dir
Given a string containing the path of a file or directory, this filter will return
the parent directorys path.
RealPath
Compress
Compresses the input data with the specified algorithm (GZ by default).
239
Class name
Description
Decompress
Decompresses the input data with the specified algorithm (the effect is inverse
to the Compress filter).
Encrypt
Decrypt
Decrypts the input data previously encrypted with the specified cryptographic
algorithm.
Inflector
PregReplace
StringToLower
StringToUpper
StringTrim
Removes white spaces (space, tabs, etc.) from the beginning and the end of the
string.
StripNewlines
Removes new line characters from string (ASCII codes #13, #10).
HtmlEntities
StripTags
UriNormalize
Converts a URL string to the normalized form and prepends the schema part
(e.g., converts www.example.com to http://www.example.com).
Callback
FilterChain
StaticFilter
240
Method name
Description
__construct($charlistOrOptions)
filter($value)
setCharList($charList)
getCharList()
As you can see from the table, the StringTrim filter, in addition to the filter() method, provides
the constructor method which you can (optionally) pass with the complete list of options to
initialize the filter, and the setCharList() and getCharList() methods which can be used for
setting specific filter options.
All standard filters have the constructor method (optionally) accepting an array of
options for configuring the filter when instantiating it manually.
Below, we provide two code examples showing equivalent methods of manually creating an
instance of the StringTrim filter, setting its options, and filtering a value.
Example 1. Passing options to the constructor method.
241
1
2
3
4
5
6
7
8
9
10
11
<?php
// Optionally, define a short alias for the filter class name.
use Zend\Filter\StringTrim;
// Create an instance of the filter, passing options to the constructor.
$filter = new StringTrim(array('charlist'=>"\r\n\t "));
// Perform the trimming operation on the string.
$filteredValue = $filter->filter(' [email protected]
');
In the code above, we create the StringTrim filter object with the help of the new operator (line 6).
We pass the array of options to the constructor to set the list of characters the filter will remove
(here, we tell the filter to remove the new line characters, the tabulation character, and the space
character). Actually, passing the array of options to this filter can be omitted because the filter
already has some default character list to strip off.
In line 9, we call the filter() method and pass it the string value [email protected] to be
trimmed. The expected output of this call is the [email protected] string.
Example 2. Without passing options to the constructor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
// Optionally, define a short alias for the filter class name.
use Zend\Filter\StringTrim;
// Create an instance of the filter.
$filter = new StringTrim();
// Specify which characters to remove.
$filter->setCharList("\r\n\t ");
// Perform the trimming operation on the string
$filteredValue = $filter->filter(' [email protected]
');
In the code above, we create the StringTrim filter object with the help of the new operator (line
6).
In line 9, we (optionally) call the StringTrim filters setCharList() method to set the list of
characters the filter will remove (here, we tell the filter to remove the new line characters, the
tabulation character, and the space character). This call is optional because the filter already has
some default character list for stripping off.
And, in line 12, we call the filter() method and pass it the string value [email protected]
to be trimmed. The expected output of this call is the [email protected] string.
242
<?php
// Create and execute the StringTrim filter through the StaticFilter proxy.
$filteredValue = \Zend\Filter\StaticFilter::execute(' [email protected] ',
'StringTrim', array('charlist' => "\r\n\t "));
// The expected output of the filter is the '[email protected]' string.
The StaticFilter class provides the execute() static method, which takes three arguments: the
input value, the name of the filter to apply, and the array of filter-specific options.
In line 3, we call the execute() method to automatically create the StringTrim filter, call its
setCharList() method, and pass the input value to its filter() method. This is very useful
because it can be accomplished in a single line of code.
<?php
// It is assumed that you call the following code inside of the form model's
// addInputFilter() method.
$inputFilter->add(array(
// ...
'filters' => array(
array(
'name' => 'StringTrim',
'options' => array(
'charlist' => "\r\n\t "
)
),
),
// ...
);
243
In the code above, we call the add() method provided by the InputFilter container class (line
5). The add() method takes an array which has the filters key. You typically register the filters
under that key (line 7). Filters registered under that key are inserted in a filter chain in the order
they appear in the list.
A filter configuration typically consists of the name (line 9) and options (line 10). The name is a
fully qualified filter class name (e.g., \Zend\Filter\StringTrim) or its short alias (StringTrim).
The options is an array consisting of filter-specific options. When the factory class instantiates
the filter, it passes the list of options to the filters constructor method, and the constructor
initializes the filter as needed.
A standard filters alias is typically the same as the class name. For example, the class
Zend\Filter\StringTrim has the short alias StringTrim.
The filter plugin manager is internally used by the InputFilter container class for instantiating
the standard filters.
244
Below, you can find a code example illustrating the usage of the Int filter.
1
2
3
4
5
6
7
<?php
// Create Int filter.
$filter = new \Zend\Filter\Int();
// Filter a value casting it to an integer number.
$filteredValue = $filter->filter('10'); // Returns (int) 10.
$filteredValue2 = $filter->filter(array('10', '20')); // Returns array as is.
In the code above, we pass the string 10 to the filter (line 6). The expected return value is the
integer 10.
In line 7, we pass an array to the filter. Because the Int filter works with scalar values only, it
returns the array as is (without changes) and raises a PHP warning.
8.6.1.2 Boolean Filter
The Boolean class is a filter that is designed to cast an arbitrary data to a boolean value (true or
false). This filter can be used for filtering check box form fields.
Its public methods are listed in table 8.3.
245
Method name
Description
filter($value)
setCasting($flag)
getCasting()
setType($type)
getType()
setTranslations($translations)
getTranslations()
The filter provides several methods allowing to set filtering options (setCasting(), setType(),
and setTranslations()).
The setCasting() method allows to choose one of two modes in which the filter may operate.
If the flag is true, the filter will behave like the PHP (boolean) cast operator. Otherwise (if the
flag is set to false), it will cast only from types defined by the setType() method, and all other
values will be returned as is.
The setType() filters method allows to define from which types to cast. This method accepts
the single argument $type, which can be either an OR combination of TYPE_-prefixed constants
or an array containing the literal equivalents of the constants. Possible constants accepted by the
setType() method and their literal equivalents are listed in table 8.4:
Table 8.4. Type constants
Constant
Numeric Value
Literal Equivalent
Description
TYPE_BOOLEAN
TYPE_INTEGER
TYPE_FLOAT
TYPE_STRING
TYPE_ZERO_STRING
1
2
4
8
16
boolean
integer
float
string
zero
TYPE_EMPTY_ARRAY
TYPE_NULL
TYPE_PHP
32
64
127
array
null
php
TYPE_FALSE_STRING
128
false
TYPE_LOCALIZED
256
localized
TYPE_ALL
511
all
The following code example shows two equivalent ways you can call the setType() method:
246
<?php
use Zend\Filter\Boolean;
// Call the setType() and pass it a combination of constants.
$filter->setType(Boolean::TYPE_BOOLEAN|
Boolean::TYPE_INTEGER|
Boolean::TYPE_STRING);
// Call the setType() and pass it an array with literal equivalents.
$filter->setType(array('boolean', 'integer', 'string'));
The setTranslations() method allows to define localized equivalents of boolean true and
false values. This method accepts a single parameter, which must be an array in the form of
keyvalue pairs, where the key is a localized string and the value is its boolean representation.
The following code example shows how to use the setTranlsations() method:
<?php
$filter->setTranslations(array(
'yes' => true,
// English 'yes'
'no' => false,
// English 'no'
'ja' => true,
// German 'yes'
'nicht' => false, // German 'no'
'' => true,
// Russian 'yes'
'' => false
// Russian 'no'
));
Below, we provide a code example illustrating the usage of the Boolean filter.
<?php
// Create Boolean filter.
$filter = new \Zend\Filter\Boolean();
// Optionally configure the filter.
$filter->setCasting(true);
$filter->setType(\Zend\Filter\Boolean::TYPE_ALL);
$filter->setTranslations(array('yes'=>true, 'no'=>false));
// Filter a value casting it to a boolean number.
$filteredValue = $filter->filter('false'); // Returns boolean false.
$filteredValue2 = $filter->filter('1'); // Returns boolean true.
$filteredValue3 = $filter->filter('false'); // Returns boolean false.
$filteredValue4 = $filter->filter('yes'); // Returns boolean true.
247
Method name
Description
filter($value)
setType($type)
getType()
By default, the Null filter behaves like PHPs empty() function: if the empty() function returns
a boolean true on the input data, then the filter will return the null value on that data, as well.
The setType() method can be used to set the type from which the filter will cast to null. This
method takes a single parameter, which can either be a combination of TYPE_-prefixed constants
listed in table 8.6 or an array of their literal equivalents.
Table 8.6. Type constants
Constant
Numeric Value
Literal Equivalent
Description
TYPE_BOOLEAN
boolean
TYPE_INTEGER
TYPE_EMPTY_ARRAY
TYPE_STRING
TYPE_ZERO_STRING
2
4
8
16
integer
array
string
zero
TYPE_FLOAT
TYPE_ALL
32
63
float
all
The following code example illustrates two equivalent ways you can call the setType() method:
<?php
use Zend\Filter\Null;
// Call the setType() and pass it a combination of constants.
$filter->setType(Null::TYPE_ZERO_STRING|Null::TYPE_STRING);
// Call the setType() and pass it an array with literal equivalents.
$filter->setType(array('zero', 'string'));
Below, a code example showing how to use the Null filter is provided:
248
<?php
// Create Null filter.
$filter = new \Zend\Filter\Null();
// Optionally configure the filter.
$filter->setType(\Zend\Filter\Null::TYPE_ALL);
$filteredValue = $filter->filter('0'); // Returns null.
$filteredValue2 = $filter->filter('1'); // Returns string '1'.
$filteredValue3 = $filter->filter(false); // Returns null.
Method name
Description
__construct($options)
filter($value)
setFormat($format)
In the code example below, we show how to create the filter, pass it a string date, and convert it
to the desired format:
<?php
// Create DateTimeFormatter filter.
$filter = new \Zend\Filter\DateTimeFormatter();
// Set filter's format (optional).
$filter->setFormat('F j, Y g:i A');
// Transform the date to the specified format.
$filteredValue = $filter->filter('2014-03-22 15:36');
// The expected output is 'March 22, 2014 3:36 PM'.
249
Internally, the DateTimeFormatter filter uses the DateTime class from the PHP standard
library for converting and formatting dates. For available date formats, please refer to
the PHP documentation for the DateTime class.
The BaseName filter will not process a non-scalar value. If you pass it an array, it will
return the array as is and raise a PHP warning.
Below, a code example demonstrating the usage of the Dir filter is provided.
250
<?php
// Create Dir filter.
$filter = new \Zend\Filter\Dir();
// Filter a file path and return its directory name part.
$filteredValue = $filter->filter('/var/log/httpd/error.log');
// The expected filter's output is the '/var/log/httpd'.
Method name
Description
__construct($options)
filter($value)
setExists($flag)
getExists()
The RealPath filter returns a boolean false on failure, e.g., if the file does not exist. If a
nonexisting path is allowed, you can call the setExists() method with the false parameter.
Below, a code example demonstrating the usage of the RealPath filter is provided.
<?php
// Create RealPath filter.
$filter = new \Zend\Filter\RealPath();
// Filter a file path (it is assumed that the current
// working directory is /var/log/httpd and that it contains
// the error.log file).
$filteredValue = $filter->filter('./error.log');
// The expected filter's output is the '/var/log/httpd/error.log'.
251
The RealPath filter will not process a non-scalar value. If you pass it an array, it will
return the array as is.
Method name
Description
__construct($options)
filter($value)
getAdapter()
getAdapterName()
setAdapter($adapter)
getAdapterOptions()
setAdapterOptions($options)
getOptions($option)
The Compress filter itself cannot compress data. Instead, it uses a so-called adapter class. The
adapter class must implement the CompressionAlgorithmInterface interface. You attach an
adapter to the Compress filter, and the adapter implements the concrete compression algorithm.
There are several standard adapter classes available (see figure 8.2 and table 8.10 below). Those
classes live in the Zend\Filter\Compress namespace.
Table 8.10. Compression adapters
Class name
Description
Bz2
Gz
Zip
http://www.bzip.org/
http://www.gzip.org/
252
Class name
Description
Tar
Tarball file format is now commonly used to collect many files into one larger file for
archiving while preserving file system information such as user and group
permissions, dates, and directory structures. Widely used in Linux operating system.
LZF is a very fast compression algorithm, ideal for saving space with only slight speed
cost.
Snappy is a fast data compression and decompression library developed by Google
based on ideas from LZ77.
RAR is an archive file format that supports data compression, error recovery, and file
spanning.
Lzf
Snappy
Rar
Below, a code example demonstrating the usage of the Compress filter is provided.
http://www.gnu.org/software/tar/tar.html
https://code.google.com/p/snappy/
1
2
3
4
5
6
7
8
9
10
11
12
13
253
<?php
// Create Compress filter.
$filter = new \Zend\Filter\Compress();
// Configure the adapter.
$filter->setAdapter('Zip');
$filter->setAdapterOptions(array(
'archive' => 'example.zip',
));
// Compress an input data (it is assumed that you have the testfile.txt
// file in the current working directory.
$filter->filter('testfile.txt');
In the code above, we create the instance of the Compress filter (line 3), set its adapter (line 6),
set adapters options (line 7), and finally, compress the input file (line 13). The expected result,
the example.zip archive file, will be created in the current directory. The archive will contain the
testfile.txt file.
The Decompress filter is a mirror reflection of the Compress filter and can be used by
analogy. By that reason, we do not cover theDecompress filter in this section.
Method name
Description
__construct($options)
filter($value)
getAdapter()
setAdapter($adapter)
The Encrypt filter uses adapter classes to perform actual data encryption. You attach an adapter
to the Encrypt filter with the setAdapter() method, and the adapter performs the concrete
encryption. An adapter class must implement the EncryptionAlgorithmInterface interface.
There are several standard adapter classes available (see figure 8.3 below). Those classes live in
the Zend\Filter\Encrypt namespace.
BlockCipher implements symmetric block cipher algorithm.
Openssl uses an encryption algorithm from the OpenSSL library.
254
Below, a code example demonstrating the usage of the Encrypt filter is provided.
1
2
3
4
5
6
7
8
9
<?php
// Create Encrypt filter.
$filter = new \Zend\Filter\Encrypt();
// Set encryption adapter.
$filter->setAdapter('BlockCipher');
// Encrypt an input data.
$filteredValue = $filter->filter('some data to encrypt');
Method name
Description
__construct($options)
filter($value)
setEncoding($encoding)
getEncoding()
255
By default, the filter behaves like the strtolower() PHP function. Given a string, it returns
the string with all alphabetic characters converted to lowercase. The alphabetic characters
are determined by the system locale. This means that in, for example, the default C locale,
characters such as umlaut-A () will not be converted.
Calling the setEncoding() method on the filter and passing it an encoding to use forces this filter
to behave like the mb_strtolower() PHP function. By contrast to strtolower(), alphabetic
is determined by the Unicode character properties. Thus, the behavior of this function is not
affected by locale settings, and it can convert any characters that have alphabetic property,
such as A-umlaut ().
If the value provided is non-scalar, the value will remain unfiltered, and an E_USER_WARNING will be raised indicating it cannot be filtered.
Below, a code example showing how to use the StringToLower filter is provided:
<?php
// Create StringToLower filter.
$filter = new \Zend\Filter\StringToLower();
// (Optionally) set encoding on the filter.
$filter->setEncoding('UTF-8');
// Filter a string.
$filteredValue = $filter->filter('How to Start a Business in 10 Days');
// The expected filter's output is the 'how to start a business in 10 days'.
The StringToUpper filter (converting a string to uppercase letters) is a mirror reflection of the StringToLower filter and can be used by analogy. By that reason, we do
not cover the StringToUpper filter in this section.
256
Method name
Description
__construct($options)
filter($value)
setPattern($pattern)
getPattern()
setReplacement($replacement)
getReplacement()
Below, a code example showing how to use the StringToLower filter is provided:
<?php
// Create PregReplace filter.
$filter = new \Zend\Filter\PregReplace();
// Configure the filter.
$filter->setPattern("/\s\s+/");
$filter->setReplacement(' ');
// Filter a string.
$filteredValue = $filter->filter('An example
);
with
multiple
spaces.'\
// The expected filter's output is the 'An example with multiple spaces.'
Method name
Description
__construct($options)
filter($value)
getAttributesAllowed()
setAttributesAllowed($attributesAllowed)
getTagsAllowed()
setTagsAllowed($tagsAllowed)
257
Below, a code example showing how to use the StripTags filter is provided:
<?php
// Create StripTags filter.
$filter = new \Zend\Filter\StripTags();
// Configure the filter.
$filter->setTagsAllowed(array('p'));
// Filter a string.
$filteredValue = $filter->filter(
'<p>Please click the following <a href="example.com">link</a>.</p>');
// The expected filter's output is the
// '<p>Please click the following link.</p>;'
The StripTags will not process a non-scalar value. If the value passed to the filter is
non-scalar, the value will remain unfiltered.
The StripNewlines will not process a non-scalar value. If the value passed to the filter
is non-scalar, the value will remain unfiltered.
258
Method name
Description
filter($value)
setDefaultScheme($defaultScheme)
setEnforcedScheme($enforcedScheme)
259
Public methods provided by the FilterChain class are presented in table 8.16:
Table 8.16. Public methods of the FilterChain filter
Method name
Description
filter($value)
setOptions($options)
attach($callback, $priority)
attachByName($name, $options, $priority)
merge($filterChain)
getFilters()
count()
An example filter chain is shown in figure 8.4. It consists of the StringTrim filter followed by
the StripTags filter, which is then followed by the StripNewLines filter.
To construct the filter chain like in figure 8.4, we can use the following code:
260
<?php
use Zend\Filter\FilterChain;
// Instantiate the filter chain.
$filter = new FilterChain();
// Insert filters into filter chain.
$filter->setOptions(array('filters'=>array(
array('name'=>'StringTrim',
'options'=>array('charlist'=>"\r\n\t "),
'priority'=>FilterChain::DEFAULT_PRIORITY
),
array('name'=>'StripTags',
'options'=>array('tagsallowed'=>array('p')),
'priority'=>FilterChain::DEFAULT_PRIORITY
),
array('name'=>'StripNewLines',
'priority'=>FilterChain::DEFAULT_PRIORITY
)
)
));
// Execute all filters in the chain.
$filteredValue = $filter->filter(" [email protected]<html>\n ");
// The expected output is '[email protected]'.
In the code above, we instantiate the FilterChain filter with the new operator (line 5). In line 8,
we set construct the chain of filters with the setOptions() method.
The method takes an array configuration which looks the same way as in InputFilters add()
method. The array has filters key where you register the filters you want to insert into the chain.
For each attached filter, you provide the following subkeys: * name is the fully qualified class
name of the filter (e.g., ZendFilterStringTrim) or its short alias (e.g., StringTrim); * options
is an array of options passed to the filter; and * priority is the optional key which defines the
priority of the filter in the chain. Filters with higher priority are visited first. The default value
for the priority is DEFAULT_PRIORITY.
Finally, in line 20, we call the filter() method, which walks through the chain and passes the
filtered value to each filter in turn.
261
The Callback filter is designed as a wrapper for your custom filtering algorithm. For example,
this may be useful when a standard filter is not suitable, and you need to apply your own filtering
algorithm to the data.
You implement your custom filtering algorithm as a callback function or a callback
class method. A callback is a function or a public method of a class which is called by
the Callback filter and is passed the value to be filtered and, optionally, user-defined
argument(s).
The public methods provided by the Callback filter are listed in table 8.17.
Table 8.17. Public methods of the Callback filter
Class name
Description
filter($value)
setCallback($callback)
getCallback()
setCallbackParams($params)
getCallbackParams()
As you can see from the table, the Callback filter provides the setCallback() and setCallbackParams()
methods that can be used to set the callback function (or the callback class method) and,
optionally, pass it one or several parameters.
8.6.6.1 Example
To demonstrate the usage of the Callback filter, lets add the phone number field to our
ContactForm form model class and attach a custom filter to it.
An international phone number typically looks like 1 (808) 456-7890. It consists of the country
code followed by the three-digit area code enclosed into braces. The rest of the phone consists
of the seven-digit subscriber code divided in two groups separated by a dash. The country code,
the area code, and the subscriber code are separated by the space character. We will refer to this
phone format as the international format.
The international phone format is required for making telephone calls between different
countries (or areas). If the calls are made within the same area, the telephone number may simply
look like 456-7890 (we just omit the country code and area code). We will refer to this phone
format as the local phone format.
To make our filter as generic as possible, we assume that the user is required to enter the phone
in international format for some forms and in local format for other forms. Because some site
visitors may enter their phone number in a format different from what is required, we want to
apply the filter that will normalize the phone number for us.
To do the phone normalization, the filter will:
1. Strip out any non-numeric characters of the input value.
262
2. Pad the digits to the required length if there are too few digits.
3. Add the braces, the spaces, and the dash (when using the international format); or simply
add the dash (when using the local format).
Because ZF2 does not provide a standard filter for accomplishing such phone filtering operation,
we will use the Callback wrapper filter. To do that, we will make the following changes to the
code of our ContactForm class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php
// ...
class ContactForm extends Form
{
// ...
protected function addElements() {
// ...
// Add "phone" field
$this->add(array(
'type' => 'text',
'name' => 'phone',
'attributes' => array(
'id' => 'phone'
),
'options' => array(
'label' => 'Your Phone',
),
));
}
private function addInputFilter() {
// ...
$inputFilter->add(array(
'name'
=> 'phone',
'required' => true,
'filters' => array(
array(
'name' => 'Callback',
'options' => array(
'callback' => array($this, 'filterPhone'),
'callbackParams' => array(
'format' => 'intl'
)
)
),
),
)
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
263
);
}
// Custom filter for a phone number.
public function filterPhone($value, $format) {
if(!is_scalar($value)) {
// Return non-scalar value unfiltered.
return $value;
}
$value = (string)$value;
if(strlen($value)==0) {
// Return empty value unfiltered.
return $value;
}
// First, remove any non-digit character.
$digits = preg_replace('#[^0-9]#', '', $value);
if($format == 'intl') {
// Pad with zeros if the number of digits is incorrect.
$digits = str_pad($digits, 11, "0", STR_PAD_LEFT);
// Add the braces, the spaces, and the dash.
$phoneNumber = substr($digits, 0, 1) . ' ('.
substr($digits, 1, 3) . ') ' .
substr($digits, 4, 3) . '-'.
substr($digits, 7, 4);
} else { // 'local'
// Pad with zeros if the number of digits is incorrect.
$digits = str_pad($digits, 7, "0", STR_PAD_LEFT);
// Add the dash.
$phoneNumber = substr($digits, 0, 3) . '-'. substr($digits, 3, 4);
}
return $phoneNumber;
}
}
In lines 10-19 of the code above, we add the phone field to the ContactForm form model. The
field is a usual text input field, and we already had some experience of working with such fields
earlier.
Then, in lines 24-39, we add a validation rule for the phone field of our form. Under the filters
264
key (line 27), we register the Callback filter (here, we use the short alias Callback, but you can
alternatively use the fully qualified class name \Zend\Filter\Callback).
The filter takes two options (line 30): the callback option and the callback_params option.
The callback option is an array consisting of two elements, which represent the class and the
method to call, respectively. In this example, the callback is the filterPhone() method of the
ContactForm class. We pass the format parameter to the callback method with the help of
callbackParams option (line 32).
In lines 43-79, we define the filterPhone() callback method, which takes two arguments: the
$value is the phone number to filter, and the $format is the desired phone number format. The
$format parameter may either be local (for local format) or intl (for international format).
In the filterPhone() callback method, we do the following:
First, in line 45, we check if the $value parameter is a scalar and not an array. If the value
is not a scalar, we return it without change.
In line 52, we check the input values length. We do nothing if the user entered an empty
phone number; we just return it as is.
Then, we remove any non-digit characters (line 58).
If phone length is too short, we pad it with zeroes.
We add the braces, the dash, and the spaces for international phone numbers; or just the
dash for local phone numbers.
Finally, we return the resulting phone number.
To see how this filter works, you can open the http://localhost/contactus URL in your web
browser. If you enter some phone number in an incorrect format, the filter will fix the phone
number and transform it to the desired format.
We plan to have the following methods in our PhoneFilter filter class (see table 8.18):
265
Method name
Description
__construct($options)
setFormat($format)
getFormat()
filter($value)
To start, create the PhoneFilter.php file in the Application/Service directory under the modules
source directory . Put the following code into that file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
namespace Application\Service;
use Zend\Filter\AbstractFilter;
// This filter class is designed for transforming an arbitrary phone number t\
o
// the local or the international format.
class PhoneFilter extends AbstractFilter
{
// Phone format constants.
const PHONE_FORMAT_LOCAL = 'local'; // Local phone format
const PHONE_FORMAT_INTL = 'intl'; // International phone format
// Available filter options.
protected $options = array(
'format' => self::PHONE_FORMAT_INTL
);
// Constructor.
public function __construct($options = null)
{
// Set filter options (if provided).
if(is_array($options)) {
if(isset($options['format']))
$this->setFormat($options['format']);
}
}
// Sets phone format.
public function setFormat($format)
The PhoneFilter class may be considered as a service model because its goal is to process data, not to store it.
266
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
{
// Check input argument.
if( $format!=self::PHONE_FORMAT_LOCAL &&
$format!=self::PHONE_FORMAT_INTL ) {
throw new \Exception('Invalid format argument passed.');
}
$this->options['format'] = $format;
}
// Returns phone format.
public function getFormat()
{
return $this->format;
}
// Filters a phone number.
public function filter($value)
{
if(!is_scalar($value)) {
// Return non-scalar value unfiltered.
return $value;
}
$value = (string)$value;
if(strlen($value)==0) {
// Return empty value unfiltered.
return $value;
}
// First, remove any non-digit character.
$digits = preg_replace('#[^0-9]#', '', $value);
$format = $this->options['format'];
if($format == self::PHONE_FORMAT_INTL) {
// Pad with zeros if the number of digits is incorrect.
$digits = str_pad($digits, 11, "0", STR_PAD_LEFT);
// Add the braces, the spaces, and the
$phoneNumber = substr($digits, 0, 1) .
substr($digits, 1, 3) .
substr($digits, 4, 3) .
substr($digits, 7, 4);
} else { // self::PHONE_FORMAT_LOCAL
dash.
' (' .
') ' .
'-' .
79
80
81
82
83
84
85
86
87
88
267
From line 2, you can see that the filter class lives in the Application\Service namespace.
In line 8, we define the PhoneFilter class. We derive our filter class from the AbstractFilter
base class to reuse the functionality it provides. Line 4 contains the short alias for the AbstractFilter
class.
In lines 11-12, for convenience, we define the phone format constants (PHONE_FORMAT_INTL for
international format and PHONE_FORMAT_LOCAL for local format). These are the equivalents of the
intl and local strings, respectively.
In lines 15-17, we define the $options private variable, which is an array having the single key
named format. This key will contain the phone format option for our filter.
In lines 20-28, we have the constructor method, which takes the single argument $options.
When constructing the filter manually, you may omit this parameter. However, when the filter
is constructed by the factory class, the factory will pass filter options to the filters constructor
through this argument.
In lines 31-37 and 43-46, we have the setFormat() and getFormat() methods that allow to set
and retrieve the current phone format, respectively.
In lines 49-86, we have the filter() method. This method encapsulates the phone number
filtering algorithm. It takes the $value parameter, transforms it by taking the selected phone
format in account, and returns the formatted phone number.
268
$inputFilter->add(array(
'name'
=> 'phone',
'required' => true,
'filters' => array(
array(
'name' => '\Application\Service\PhoneFilter',
'options' => array(
'format' => \Application\Service\PhoneFilter::PHONE_FORMAT_INTL
)
),
// ...
),
// ...
)
);
You can see how the PhoneFilter filter works in the Form Demo sample application bundled
with this book. Open the http://localhost/contactus page in your web browser. If you enter
some phone number in an incorrect format, the filter will fix the phone number.
If you wish, you can use the PhoneFilter outside of forms, as shown in the code example below:
<?php
use Application\Service\PhoneFilter;
// Create PhoneFilter filter.
$filter = new PhoneFilter();
// Configure the filter.
$filter->setFormat(PhoneFilter::PHONE_FORMAT_INTL);
// Filter a string.
$filteredValue = $filter->filter('12345678901');
// The expected filter's output is the '1 (234) 567-8901'.
8.8 Summary
Filters are designed to take some input data, process it, and produce some output data. Zend
Framework 2 provides a lot of standard filters that can be used for creating filtering rules of your
forms (or, if you wish, to filter an arbitrary data outside of forms).
The standard filters can be roughly divided into several groups:
filters casting input data to a specified type;
filters performing manipulations on a file path;
269
Description
Zend\Validator
Zend\InputFilter
9.1.1 ValidatorInterface
In ZF2, a validator is a usual PHP class which implements the ValidatorInterface interface (it
belongs to Zend\Validator namespace). The interface definition is presented below:
1
2
3
4
5
6
7
8
9
<?php
namespace Zend\Validator;
interface ValidatorInterface
{
// Returns true if and only if $value meets the validation requirements.
public function isValid($value);
// Returns an array of messages that explain why
10
11
12
271
As you can see, the ValidatorInterface has two methods: the isValid() method (line 7) and
getMessages() method (line 11).
The first one, isValid() method, is intended to perform the check of the input value (the $value
parameter). If the validation of the $value passes, the isValid() method returns boolean true.
If the $value fails validation, then this method returns false.
A concrete validator class implementing the ValidatorInterface interface may have
additional methods. For example, many validator classes have methods allowing to
configure the validator (set validation options).
272
Class name
EmailAddress
Description
Returns boolean true if the value is a valid E-mail address; otherwise returns
false.
Hostname
Barcode
Returns boolean true if and only if the value contains a valid barcode.
CreditCard
Returns true if and only if the value follows the common format of credit card
number (Luhn algorithm, mod-10 checksum).
Iban
273
Class name
Description
Isbn
Ip
Uri
Returns true if and only if the value is an Uniform Resource Identifier (URI).
Between
Returns true if the value lies in certain range; otherwise returns false.
LessThan
GreaterThan
Returns boolean true if the value is less than certain number; otherwise
returns false.
Returns true if and only if value is greater than certain number.
Identical
Step
Csrf
This validator checks if the provided token matches the one previously
generated and stored in a PHP session.
Date
DateStep
InArray
Returns true if value is contained in the given array; otherwise returns false.
Digits
Returns boolean true if and only if $value only contains digit characters.
Hex
Returns true if and only if value contains only hexadecimal digit characters.
IsInstanceOf
NotEmpty
Regex
Returns true if value matches against given pattern; otherwise returns false.
StringLength
Explode
Splits the given value in parts and returns true if all parts pass the given check.
StaticValidator
Callback
ValidatorChain
274
below for possible validation errors that the EmailValidator returns if you pass it the abc@ewr
value (the back-slash () character indicates line breaks where code doesnt fit book page):
array(3) {
["emailAddressInvalidHostname"] =>
string(51) "'ewr' is not a valid hostname for \
the email address"
["hostnameInvalidHostname"] =>
string(66) "The input does not match the expec\
ted structure for a DNS hostname"
["hostnameLocalNameNotAllowed"] =>
string(84) "The input appears to be a local ne\
twork name but local network names are not allowed"
}
Validators getMessages() method will return an array of messages that explain why the
validation failed. The array keys are validation failure message identifiers, and the array values
are the corresponding human-readable message strings.
If isValid() method was never called or if the most recent isValid() call returned true, then
the getMessages() method returns an empty array. Also, when you call isValid() several times,
the previous validation messages are cleared, so you see only validation errors from the last call.
Some validators may work with input data in certain format only (for example, a validator may
require that the input data be a string, but not an array). If you pass it data in unacceptable
format, the validator may throw an Zend\Validator\Exception\RuntimeException exception
or raise a PHP warning.
It is recommended to check certain validators documentation to be aware of its actual
behaviour in case of unacceptable data.
275
The methods provided by the EmailAddress validator are listed in table 9.2:
Table 9.2. Public methods of the EmailAddress validator
Method name
Description
__construct($options)
isValid($value)
getMessages()
useDomainCheck($domain)
getDomainCheck()
setHostnameValidator($hostnameValidator)
getHostnameValidator()
setAllow($allow)
getAllow()
useMxCheck($mx)
getMxCheck($mx)
useDeepMxCheck($deep)
getDeepMxCheck()
isMxSupported()
https://tools.ietf.org/html/rfc2822
An MX record is a type of record used in the Domain Name System (DNS). MX records define one or several mail server addresses assigned
to recipients domain.
276
Method name
Description
getMXRecord()
As you can see from table, the EmailAddress validator, additionally to the isValid() and
getMessages() methods, provides the constructor method to which you can (optionally) pass
the complete list of options for initializing the validator.
All standard validators have the constructor method (optionally) accepting an array of
options for configuring the validator when instantiating it manually.
The EmailAddress class also provides a number of methods that can be used for setting specific
validator options.
The useDomainCheck() method tells whether to check the host name for correctness, or not. By
default, this check is enabled. The setAllow() method provides an ability to specify which types
of host names are allowed. You can pass an OR combination of the ALLOW_-prefixed constants
to the setAllow() method:
Internally, the EmailAddress validator uses the Hostname validator for checking the
host name part of an E-mail address. Optionally, you can attach a custom host name
validator by using the setHostnameValidator() method, however it is unlikely you
will need to do such.
The useMxCheck() method tells whether the validator should connect to the recipients host and
query the DNS server for the MX record(s). If the server has no MX records, than the validation
fails. You can additionally use the useDeepMxCheck() method to tell the validator to compare the
mail server addresses extracted from the MX records against the black list of reserved domain
names, and perform additional checks per each detected address.
It is not recommended to perform MX check (and deep MX check), because that may
take a lot of time and increase the web page load time. By default, these checks are
disabled.
Below, we provide code examples showing two equivalent methods of manual creating of an
instance of the EmailAddress validator, setting its options and checking an input value:
Example 1. Passing options to the constructor method.
The ALLOW_-prefixed constants are provided by the Hostname validator.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
277
<?php
// Optionally, define a short alias for the validator class name.
use Zend\Validator\EmailAddress;
use Zend\Validator\Hostname;
// Create an instance of the validator, passing options to the constructor.
$validator = new EmailAddress(array(
'allow' => Hostname::ALLOW_DNS|Hostname::ALLOW_IP|Hostname::ALLOW_LOCAL,
'mxCheck' => true,
'deepMxCheck' => true
));
// Validate an E-mail address.
$isValid = $validator->isValid('[email protected]'); // Returns true.
$isValid2 = $validator->isValid('abc'); // Returns false.
if(!$isValid2) {
// Get error messages in case of validation failure.
$errors = $validator->getMessages();
}
In the code above, we create the EmailAddres validator object with the help of the new operator
(line 7). We pass the array of options to the constructor. We use the allow key to allow an E-mail
address to be a domain name, an IP address or local network address. Also, we use the mxCheck
and deepMxCheck to enable MX record check and deep MX record check, respectively.
In line 14, we call the isValid() method and pass it the string value [email protected] to
be checked. The expected output of this call is the boolean true.
In line 15, we pass the abc string value to the validator. The validation procedure is expected to
fail (false is returned). Then, the error messages are retrieved with the getMessages() method
(line 19).
Example 2. Without passing options to the constructor.
1
2
3
4
5
6
7
8
9
10
11
12
<?php
// Optionally, define a short alias for the validator class name.
use Zend\Validator\EmailAddress;
use Zend\Validator\Hostname;
// Create an instance of the validator.
$validator = new EmailAddress();
// Optionally, configure the validator
$validator->setAllow(
Hostname::ALLOW_DNS|Hostname::ALLOW_IP|Hostname::ALLOW_LOCAL);
$validator->useMxCheck(true);
13
14
15
16
17
18
19
20
21
22
278
$validator->useDeepMxCheck(true);
// Validate an E-mail address.
$isValid = $validator->isValid('[email protected]'); // Returns true.
$isValid2 = $validator->isValid('abc'); // Returns false.
if(!$isValid2) {
// Get error messages in case of validation failure.
$errors = $validator->getMessages();
}
In the code above, we create the EmailAddres validator object with the help of the new operator
(line 7).
In lines 10-13, we configure the validator. We call the setAllow() method to allow an Email address to be a domain name, an IP address or local network address. Also, we use the
useMxCheck() and useDeepMxCheck() to enable MX record check and deep MX record check,
respectively.
In line 16, we call the isValid() method and pass it the string value [email protected] to
be checked. The expected output of this call is the boolean true.
In line 17, we pass the abc string value to the validator. The validation procedure is expected
to fail. Then, the error messages are retrieved with the getMessages() method (line 21).
<?php
// Create and execute the EmailAddress validator through StaticValidator prox\
y.
$validatedValue = \Zend\Filter\StaticValidator::execute('[email protected]',
'EmailAddress',
array(
'allow' =>
Hostname::ALLOW_DNS|
Hostname::ALLOW_IP|
Hostname::ALLOW_LOCAL,
'mxCheck' => true,
'deepMxCheck' => true
));
// The expected output is boolean true.
279
The StaticValidator class provides the execute() static method which takes three arguments:
the input value, the name of the filter to apply, and the array of filter-specific options.
In line 3, we call the execute() method to automatically create the EmailAddress validator, call
its setAllowDns(), useMxCheck() and useDeepMxCheck() methods, and pass the input value to
its isValid() method. This is very useful, because can be accomplished in a single call.
The StaticValidator doesnt provide an ability to extract the list of human-readable
validation errors. However, since the StaticValidator is designed to be used outside
forms, and not intended for displaying results to a human, this seems to be not a big
disadvantage.
<?php
// It is assumed that you call the following code inside of the form model's
// addInputFilter() method.
$inputFilter->add(array(
// ...
'validators' => array(
array(
'name' => 'EmailAddress',
'options' => array(
'allow' => \Zend\Validator\Hostname::ALLOW_DNS,
'useMxCheck' => false,
'useDeepMxCheck' => false,
),
),
),
// ...
);
In the code above, we call the add() method provided by the InputFilter container class (line
5). The add() method takes an array which has the validators key. You typically register the
validators under that key (line 7). Validators registered under that key are inserted into validator
chain in the order they appear in the list.
280
A validator configuration typically consists of the name (line 9) and options (line 10). The name
is a fully qualified validator class name (e.g. \Zend\Validator\EmailAddress) or its short alias
(EmailAddress). The options is an array consisting of validator-specific options. When the
factory class instantiates the validator, it passes the list of options to the validators constructor,
and the constructor initializes the validator as needed.
A standard validators alias is typically the same as class name. For example, the class
Zend\Validator\EmailAddress has the short alias EmailAddress.
The validator plugin manager is internally used by the InputFilter container class for instantiating the standard validators.
281
Method name
Description
__construct($options)
isValid($value)
getMessages()
setOptions($options)
By default all the above are allowed, except the IPv6 literal address.
Below, a code example demonstrating the usage of the Ip validator is provided.
<?php
use Zend\Validator\Ip;
// Create Ip validator.
$validator = new Ip();
// Configure the validator.
$validator->setOptions(array(
'allowipv4'
=> true,
'allowipv6'
=> true,
'allowipvfuture' => false,
'allowliteral'
=> true,
);
//
//
//
//
Allow
Allow
Allow
Allow
IPv4 addresses.
IPv6 addresses.
IPvFuture addresses.
IP addresses in literal format.
282
Method name
Description
__construct($options)
isValid($value)
getMessages()
setIpValidator($ipValidator)
getIpValidator()
setAllow()
getAllow()
useIdnCheck()
getIdnCheck()
useTldCheck()
getTldCheck()
You can set which host name types are allowed with the setAllow() method. It accepts a
combination of the following constants:
283
4. Each domain part is checked based on the rules for acceptable domain names. If a domain
name is an IDN , it is checked against the rules for valid IDNs. (You can disable IDN check
with useIdnCheck() method.)
Below, a code example demonstrating the usage of the Hostname validator is provided.
<?php
use Zend\Validator\Hostname;
// Create the Hostname validator.
$validator = new Hostname();
// Configure the validator.
$validator->setAllow(Hostname::ALLOW_DNS|Hostname::ALLOW_IP);
// Check a host name.
$isValid = $validator->validate('site1.example.com');
// Returns true.
$isValid2 = $validator->validate('abc');
// Returns false (not a valid host name).
The public methods provided by the Uri validator are listed in table 9.5:
Table 9.5. Public methods of the Uri validator
Method name
Description
__construct($options)
isValid($value)
getMessages()
setUriHandler($uriHandler)
getUriHandler()
setAllowAbsolute($allowAbsolute)
An internationalized domain name (IDN) is an Internet domain name that contains at least one label that is displayed, in whole or in part,
in a language-specific script or alphabet, like Arabic, Chinese or Russian.
A Uniform Resource Identifier (URI) is a compact sequence of characters that identifies an abstract or physical resource. An Uniform
Resource Locator (URL) is a type of URI. But, that doesnt mean all URIs are URLs.
284
Method name
Description
getAllowAbsolute()
setAllowRelative($allowRelative)
getAllowRelative()
Internally, the Uri validator uses so called URI handler object, which is responsible for parsing an
URI string. By default, Zend\Uri\Uri class is used as the URI handler. (You can set your custom
URI handler with the setUriHandler() method, if you wish.)
An URI can be absolute or relative. For example, an absolute URI is http://example.com/blog/2014/02/02/edit,
while a relative URI is 2014/02/02/edit. You can specify whether the validator should consider
absolute and/or relative URIs acceptable. For that, you use the setAllowAbsolute() and
setAllowRelative() methods, respectively. By default, both are treated as acceptable URI types.
Below, a code example demonstrating the usage of the Uri validator is provided.
<?php
use Zend\Validator\Uri;
// Create the Uri validator.
$validator = new Uri();
// Configure the validator.
$validator->setAllowAbsolute(true);
$validator->setAllowRelative(true);
// Check an URI.
$isValid = $validator->validate(
'http://site1.example.com/application/index/index');
// Returns true.
$isValid2 = $validator->validate('index/index');
// Returns true.
285
Method name
Description
__construct($options)
isValid($value)
getMessages()
setFormat($format)
getFormat()
To set the expected date format, you can use the setFormat() method.
Internally, the DateTimeFormatter filter uses the DateTime class from the PHP standard
library for converting and formatting dates. For available date formats, please refer to
the PHP documentation for the DateTime class.
Below, a code example demonstrating the usage of the Date validator is provided.
<?php
use Zend\Validator\Date;
// Create validator instance.
$validator = new Date();
// Configure validator.
$validator->setFormat('Y-m-d');
// Check if the input value is a date having expected format.
$isValid = $validator->isValid('2014-04-04'); // Returns true.
$isValid2 = $validator->isValid('April 04, 2014');
// Returns false (format is unexpected).
Method name
Description
__construct($options)
isValid($value)
getMessages()
286
Method name
Description
setPattern($pattern)
getPattern()
The setPattern() method allows to set the regular expression to match against.
For regular expressions syntax and examples, it is recommended that your refer to the
PCRE Patterns section of the PHP documentation.
Below, a code example demonstrating the usage of the Regex validator is provided. It uses the
regular expression to check if the input string is a valid IPv4 address (such address typically
consists of four groups of digits separated with dots).
<?php
use Zend\Validator\Regex;
// Create Regex validator.
$validator = new Regex();
// Set regular expression to check for an IP address.
$validator->setPattern('\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b');
// Check for regular expression match.
$isValid = $validator->isValid("127.0.0.1"); // returns true.
$isValid2 = $validator->isValid("123"); // returns false.
287
Method name
Description
__construct($options)
isValid($value)
getMessages()
setType($type)
getType()
getDefaultType()
The setType() method specifies which variable types to consider as an empty value. This method
accepts the single argument $type which can be either an OR combination of the constants listed
in table 9.9, or an array containing the literal equivalents of those constants.
Table 9.9. Type constants
Constant
Numeric Value
Literal Equivalent
Description
BOOLEAN
boolean
INTEGER
FLOAT
STRING
2
4
8
integer
float
string
ZERO
16
zero
EMPTY_ARRAY
32
array
NULL
PHP
64
127
null
php
SPACE
128
space
OBJECT
256
object
OBJECT_STRING
512
objectstring
OBJECT_COUNT
1024
objectcount
ALL
2047
all
Below, a code example demonstrating the usage of the NotEmpty validator is provided.
288
<?php
use Zend\Validator\NotEmpty;
// Create validator instance.
$validator = new NotEmpty();
// Configure validator.
$validator->setType(NotEmpty::ALL);
// Check if
$isValid1 =
$isValid2 =
$isValid3 =
Method name
Description
__construct($options)
isValid($value)
getMessages()
setMin($min)
getMin()
setMax($max)
getMax()
setInclusive($inclusive)
getInclusive()
The range can be set with the setMin() and setMax() methods.
By default, the validator performs inclusive comparisons (to check if the value belongs to the
given range, it compares if the value is greater or equal to its lower bound, and if the value is
lower or equal to its upper bound). You can change this with the setInclusive() method. It tells
the validator whether to perform inclusive comparisons (pass true as the argument) or exclusive
comparisons (pass false as the argument).
Below, a code example demonstrating the usage of the Between validator is provided.
289
<?php
use Zend\Validator\Between;
// Create validator instance.
$validator = new Between();
// Configure validator.
$validator->setMin(1);
$validator->setMax(10);
$validator->setInclusive(true);
$isValid1
$isValid2
$isValid3
$isValid4
=
=
=
=
Method name
Description
__construct($options)
isValid($value)
getMessages()
setHaystack($haystack)
getHaystack()
setStrict($strict)
getStrict()
setRecursive($recursive)
getRecursive()
The setHaystack() method allows to set the array of allowed values. The isValid() method
will search through this array for the presence of the input $value.
If the array contains nested values and you want to search among them recursively, then use
setRecursive() method. This method takes the single boolean flag. If the flag is true, than the
search will be performed recursively; otherwise the nested levels will be ignored.
The setStrict() method provides an ability to tell the validator how to compare the input value
and the values in array. This may be a combination of the following constants:
COMPARE_NOT_STRICT Do not perform strict check of variable type.
COMPARE_NOT_STRICT_AND_PREVENT_STR_TO_INT_VULNERABILITY Do not perform strict check
of variable type, but prevent false positive comparisons of string to integer (e.g. "asdf" ==
0). This is the default option.
290
Method name
Description
__construct($options)
isValid($value)
getMessages()
setMin($min)
getMin()
setMax($max)
getMax()
setEncoding($encoding)
getEncoding()
By default, the StringLength validator considers any string length as valid. Use the setMin()
and/or setMax() methods to set lower and upper limits for the allowable string length. There are
three possible ways you can do that:
Use only the setMin() method to allow strings with a lower-bound minimum length, but
unbound upper length;
Use only the setMax() method to allow strings with zero minimum length and an upperbound maximum length;
291
Use both the setMin() and setMax() methods to allow strings with a length laying
between the lower and upper bounds.
By default, the PHP engine uses the UTF-8 encoding for strings. If your input string uses a
different encoding, you should specify it encoding with the setEncoding() validators method.
Below, a code example demonstrating the usage of the StringLength validator is provided.
<?php
use Zend\Validator\StringLength;
// Create validator instance.
$validator = new StringLength();
// Configure the validator.
$validator->setMin(1);
$validator->setMax(10);
$isValid1 = $validator->isValid("string"); // returns true.
$isValid2 = $validator->isValid(""); // returns false (value is too short).
$isValid3 = $validator->isValid(
"a very long string"); // returns false (value is too long).
Public methods provided by the ValidatorChain class are presented in table 9.13:
Table 9.13. Public methods of the ValidatorChain validator
Method name
Description
isValid($value)
getMessages()
getValidators()
count()
attach($validator, $breakChainOnFailure)
prependValidator($validator,
$breakChainOnFailure)
292
Method name
Description
attachByName($name, $options,
$breakChainOnFailure)
prependByName($name, $options,
$breakChainOnFailure)
merge($validatorChain)
An example validator chain is shown in figure 9.2. It consists of the NotEmpty validator followed
by the StringLength validator, which in turn is followed by the Date validator. When this chain
is executed, first, the NotEmpty validator is run checking that the value is a non-empty value,
then the StringLength validator is run checking that the length of the input string belongs to
range (1, 16), inclusively, and finally, the Date validator is run checking that the input value is a
date in format YYYY-MM-DD.
To construct the filter chain like in figure 9.2, we can use the following code:
<?php
// Instantiate the validator chain.
$validator = new \Zend\Validator\ValidatorChain();
// Insert validators into validator chain.
$validator->attachByName('NotEmpty');
$validator->attachByName('StringLength', array('min'=>1, 'max'=>16));
$validator->attachByName('Date', array('format'=>'Y-m-d'));
// Execute all validators in the chain.
$isValid = $validator->isValid('2014-04-04'); // Returns true.
293
Class name
Description
isValid($value, $context)
getMessages()
setCallback($callback)
getCallback()
setCallbackOptions($options)
getCallbackOptions()
As you can see from the table, the Callback validator provides the setCallback() and
setCallbackOptions() methods that can be used to set the callback function or class method
and (optionally) pass it one or several parameters.
9.6.4.1 Example
To demonstrate the usage of the Callback validator, lets add the phone number validator to our
ContactForm form model class. The validator would check a telephone number entered by site
visitor.
The validator needs to be able to check for two common phone number formats:
international format looking like 1 (234) 567-8901;
and local format, which looks like 567-8901.
Because ZF2 does not provide a standard validator for accomplishing such phone filtering
operation, we will use the Callback wrapper validator. To do that, we will make the following
changes to the code of our ContactForm class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
// ...
class ContactForm extends Form
{
// ..
protected function addElements() {
// ...
// Add "phone" field
$this->add(array(
'type' => 'text',
'name' => 'phone',
'attributes' => array(
'id' => 'phone'
),
'options' => array(
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
294
63
64
65
295
return ($matchCount!=0)?true:false;
}
}
In the code above, we create the phone field in our ContactForm (if you already have such field,
just ignore this).
In line 40, we add the PhoneValidator validator to the input filters validator chain for the
phone field.
In lines 45-58, we have the validatePhone() callback method. The method accepts three
arguments: the $value parameter is the phone number to validate, the $context parameter
receives the values of every field of the form (it may be needed for some validators to refer
to the values of other form fields, too); and the $format parameter is the expected format of the
phone number (intl or local).
Inside of the callback method, we do the following:
1. Calculate the correct length of the phone, check whether the length is correct for the
selected phone number format.
2. Match the phone number against the regular expression pattern for the selected phone
format.
We plan to have the following methods in our PhoneValidator validator class (see table 9.18):
Table 9.18. Public methods of the Callback validator
Method name
Description
__construct($options)
setFormat($format)
getFormat()
isValid($value)
getMessages()
296
<?php
namespace Application\Service;
use Zend\Validator\AbstractValidator;
// This validator class is designed for checking a phone number for
// conformance to the local or to the international format.
class PhoneValidator extends AbstractValidator {
// Phone format constants.
const PHONE_FORMAT_LOCAL = 'local'; // Local phone format.
const PHONE_FORMAT_INTL = 'intl'; // International phone format.
// Available validator options.
protected $options = array(
'format' => self::PHONE_FORMAT_INTL
);
// Validation failure message IDs.
const NOT_SCALAR = 'notScalar';
const INVALID_FORMAT_INTL = 'invalidFormatIntl';
const INVALID_FORMAT_LOCAL = 'invalidFormatLocal';
// Validation failure messages.
protected $messageTemplates = array(
self::NOT_SCALAR => "The phone number must be a scalar value",
self::INVALID_FORMAT_INTL
=> "The phone number must be in international format",
self::INVALID_FORMAT_LOCAL => "The phone number must be in local format",
);
The PhoneValidator class may be considered as a service model, because its goal is to process data, not to store it.
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Constructor.
public function __construct($options = null) {
// Set filter options (if provided).
if(is_array($options)) {
if(isset($options['format']))
$this->setFormat($options['format']);
}
// Call the parent class constructor.
parent::__construct($options);
}
// Sets phone format.
public function setFormat($format) {
// Check input argument.
if($format!=self::PHONE_FORMAT_LOCAL &&
$format!=self::PHONE_FORMAT_INTL) {
throw new \Exception('Invalid format argument passed.');
}
$this->options['format'] = $format;
}
// Validates a phone number.
public function isValid($value) {
if(!is_scalar($value)) {
$this->error(self::NOT_SCALAR);
return $false; // Phone number must be a scalar.
}
// Convert the value to string.
$value = (string)$value;
$format = $this->options['format'];
// Determine the correct length and pattern of the phone number,
// depending on the format.
if($format == self::PHONE_FORMAT_INTL) {
$correctLength = 16;
$pattern = '/^\d \(\d{3}\) \d{3}-\d{4}$/';
} else { // self::PHONE_FORMAT_LOCAL
297
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
298
$correctLength = 8;
$pattern = '/^\d{3}-\d{4}$/';
}
// First check phone number length
$isValid = false;
if(strlen($value)==$correctLength) {
// Check if the value matches the pattern.
if(preg_match($pattern, $value))
$isValid = true;
}
// If there were an error, set error message.
if(!$isValid) {
if($format==self::PHONE_FORMAT_INTL)
$this->error(self::INVALID_FORMAT_INTL);
else
$this->error(self::INVALID_FORMAT_LOCAL);
}
// Return validation result.
return $isValid;
}
}
From line 2, you can see that the validator class lives in the Application\Service namespace.
In line 8, we define the PhoneValidator class. We derive our validator class from the AbstractValidator
base class to reuse the functionality it provides. Line 4 contains the short alias for the AbstractValidator
class.
In lines 11-12, for convenience, we define the phone format constants (PHONE_FORMAT_INTL for
international format and PHONE_FORMAT_LOCAL for local format). These are the equivalents of the
intl and local strings, respectively.
In lines 15-17, we define the $options private array variable which is an array having the single
key named format. This key will contain the phone format option for our validator.
In lines 20-22, we define the error message identifiers. We have three identifiers (NOT_SCALAR,
INVALID_FORMAT_INTL and INVALID_FORMAT_LOCAL), because our validator may generate three
different error messages. These identifiers are intended for distinguishing different error messages by machine, not by human.
In lines 25-30, we have the $messageTemplates array variable that contains mapping before
error message identifiers and their textual representations. The textual messages are intended
for displaying to a human.
In lines 33-44, we have the constructor method which takes the single argument $options. When
constructing the validator manually, you may omit this parameter. But, when the validator is
299
constructed by the factory class, the factory will pass validation options to validators constructor
through this argument.
In lines 47-56 and 59-64, we have the setFormat() and getFormat() methods that allow to set
and retrieve the current phone format, respectively.
In lines 59-99, we have the isValid() method. This method encapsulates the phone number
checking algorithm. It takes the $value parameter, performs the regular expression match, and
returns true on success.
On failure, the isValid() method it returns the boolean false, and the list of errors can be
retrieved by the getMessages() method.
You might notice that we didnt define the getMessages() method in our
PhoneValidator class. This is because that we inherited this method from the
AbstractValidator base class. Inside of our isValid() method, for generating error
messages, we also used the error() protected method provided by the base class (lines
62, 92, 94).
You can see how the PhoneValidator validator works in the Form Demo sample application
bundled with this book. Open the http://localhost/contactus page in your web browser. If you
300
enter some phone number in an incorrect format, the validator will display an error (see figure
9.3).
If you wish, you can use the PhoneValidator outside of forms, as shown in code example below:
<?php
use Application\Service\PhoneValidator;
// Create PhoneValidator validator
$validator = new PhoneValidator();
// Configure the validator.
$validator->setFormat(PhoneValidator::PHONE_FORMAT_INTL);
// Validate a phone number
$isValid = $validator->isValid('1 (234) 567-8901'); // Returns true.
$isValid2 = $validator->isValid('12345678901'); // Returns false.
if(!$isValid2) {
// Get validation errors.
$errors = $validator->getMessages();
}
301
Lets assume we implement a payment gateway system and need to create a web page displaying
a payment history for the given credit card on given date. This page can be handled by some
paymentHistoryAction() action of a controller class, and the credit card number and date will be
extracted from GET variables. For the paymentHistoryAction() method, we need to implement
some security checks:
we want to ensure that the credit card number looks like a typical credit card number
4532-7103-4122-1359 (conforms to ISO/IEC 7812 standard);
and that the date is in YYYY-MM-DD format.
Below, you can find the code of the action method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
namespace Application\Controller;
use
use
use
use
Zend\Mvc\Controller\AbstractActionController;
Zend\View\Model\ViewModel;
Zend\Filter\StaticFilter;
Zend\Validator\StaticValidator;
302
Inside the action method, we use the params() controller plugin (lines 16-17) to retrieve two
variables from $_GET super-global array: the card variable (credit card number) and the date
variable (the date).
In line 20, we validate the credit card number with the help of the CreditCard validator. If the
card number is not acceptable, we throw an exception indicating an error (line 22).
In line 26, we use the DateTimeFormatter filter to convert the date to the right format.
9.9 Summary
Validators are designed to take some input data, check it for correctness, and return a boolean
result telling whether the data is correct (and error messages if the data has some errors).
In Zend Framework 2, there are several groups of standard validators:
validators for checking value conformance to certain format;
validators for checking a numerical value lies in a given range;
validators working as proxies to other validators.
In some cases, a standard validator is not suitable, and you need to apply your own checking
algorithm to the input data. In such case, you may use either the Callback validator or write
your own custom validator class.
Description
Zend\Form
Zend\Filter
Zend\Validator
Zend\InputFilter
HTTP file uploads are described in RFC-1867. This mechanism allows to upload large files by using binary content transfer encoding. The
multipart/form-data encoding type is utilized for this purpose.
The HTTP GET method is inefficient for file uploads, because URL length has some upper limit. Also, URL-encoding file data greatly
increases the URL length.
304
1
2
3
4
5
The file element has the Browse button allowing to pick a file for upload. When the site user
picks some file and clicks the Submit button on the form, the web browser will send an HTTP
request to the web server, and the request will contain the data of the file being uploaded. The
example below illustrates how the HTTP request may look like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
305
As you can see from the example above, the HTTP request with multipart/form-data encoding
type looks analogous to a usual HTTP request (has the status line, the headers, and the content
area), however it has the following important differences:
Line 5 sets the Content-Type header with multipart/form-data value; The form is
assembled of the fields marked by the boundary a unique randomly generated sequence
of characters delimiting form fields of each other.
Lines 8-17 represent the content of the HTTP request. The form fields are delimited by the
boundary sequences (lines 8, 13, 17). The data of the file being uploaded are transmitted
in binary format (line 12), and that allows to reduce the content size to its minimum.
By default, PHP engines settings do not allow to upload large files (larger than 2MB).
In order to upload large files, you may need to edit the php.ini configuration file
and modify the post_max_size and upload_max_filesize parameters (please refer to
Appendix A for information on how to do that). Setting these with 100M allows to
upload files up to 100 Mb in size, and this would typically be sufficient. If you plan
to upload very large files up to 1 GB in size, than better set these with 1024M. Do not
forget to restart your Apache Web Server after editing the configuration file.
For example, for the above mentioned simple upload form, the $_FILES super-global array will
look as follows (the output is generated with the var_dump() PHP function):
1
2
3
4
5
6
7
8
array (size=1)
'myfile' =>
array (size=5)
'name' => string 'somefile.txt' (length=12)
'type' => string 'text/plain' (length=10)
'tmp_name' => string 'C:\Windows\Temp\phpDC66.tmp' (length=27)
'error' => int 0
'size' => int 18
As you can see from the example above, the $_FILES array contains an entry per each uploaded
file. For each uploaded file, it contains the following information:
306
The following code example shows how to move the file uploaded with the simple form we have
considered above:
1
2
3
4
5
$destPath = '/path/to/your/upload/dir';
$result = move_uploaded_file($_FILES['myfile']['tmp_name'], $destPath);
if(!$result) {
// Some error occurred.
}
Above, in line 1, we set the the $destPath with the directory name where to save the uploaded
file.
In line 2, we call the move_uploaded_file() function and pass it two arguments: the path to the
temporary file and the destination path.
Specifying the directory name as the second argument of the move_uploaded_file()
function is suitable when you do not want to rename the file. If you need to save the
uploaded file under another name than its original name, you can specify the full file
path instead of the directory name.
In line 3, we check the returned value of the function. If the operation is successful, the function
will return true. If some error occurs (for example, if directory permissions are insufficient to
save the file), the boolean false will be returned.
MIME type, also known as content type is a standard identifier used on the Internet to indicate the type of data that a file contains. For
example the text/plain MIME type is assigned to a text file, while the application/octet-stream MIME type is assigned to a binary file.
307
<?php
//...
class IndexController extends AbstractActionController
{
// An example controller action intended for handling file uploads.
public function uploadAction() {
// Get the whole $_FILES array.
$files = $this->getRequest()->getFiles();
// The same, but with Params controller plugin.
$files = $this->params()->fromFiles();
// Get a single entry of the $_FILES array.
$files = $this->params()->fromFiles('myfile');
}
}
In line 9 of the code above, we use the getRequest() method of the controller class for accessing
the Request object, and the getFiles() method of the request object to retrieve the information
about all upload files at once.
In line 12, we do the same thing with the Params controller plugin. We use its fromFiles()
method to get the information about all uploaded files.
If needed, you can extract the information for the specific file only. In line 15, we use the same
fromFiles() method and pass it the name of the file field to retrieve. This retrieves the single
file entry from the $_FILES super-global array.
1
2
3
4
5
6
7
8
9
10
11
12
13
308
In the code above, we call the add() method provided by the Form base class and pass it the
configuration array describing the element. The type key of the array (line 5) must be either
Zend\Form\Element\File class name or its short alias file.
309
Class name
Short alias
Description
Count
FileCount
WordCount
FileWordCount
Upload
FileUpload
UploadFile
FileUploadFile
Size
FileSize
FilesSize
FileFilesSize
Extension
FileExtension
ExcludeExtension
FileExcludeExtension
MimeType
FileMimeType
ExcludeMimeType
FileExcludeMimeType
IsImage
FileIsImage
ImageSize
FileImageSize
Exists
FileExists
NotExists
FileNotExists
IsCompressed
FileIsCompressed
Hash
FileHash
Crc32
FileCrc32
Sha1
FileSha1
Md5
FileMd5
Checks that the file content has the given MD5 hash.
As you can see from the table above, file validators may be roughly divided in the following
groups:
310
validators checking whether the file(s) were really uploaded through HTTP POST and
upload status is OK;
validators checking the uploaded file count and file size;
validators checking the file extension and MIME type;
validators checking whether the file is a graphical image and checking image dimensions;
and validators checking the file hash (or check sum) .
Please note that since file validators live in Zend\Validator\File namespace, their
short aliases (that you use when creating a validator with the factory) start with File
prefix. For example, the IsImage validator has FileIsImage alias.
We will show how to use some of these file validators in the Image Gallery code example later
in this chapter.
Class name
Short alias
Description
Rename
FileRename
RenameUpload
FileRenameUpload
Encrypt
FileEncrypt
Decrypt
FileDecrypt
LowerCase
FileLowerCase
UpperCase
FileUpperCase
From the table, you can see that filters can be divided into the following groups:
filters for moving uploaded files from a temporary location to their persistent directory;
filters for encryption and decryption of files;
filters for converting text files to upper-case and lower-case letters.
Please note that since file filters live in Zend\Filter\File namespace, their short
aliases (that you use when creating a filter with the factory) start with File prefix.
For example, the RenameUpload filter has FileRenameUpload alias.
A file hash is used for checking file data integrity (for example, to ensure that file data is not corrupted). There are several hash algorithms
available (MD5, SHA-1, CRC32, etc.)
311
The Encrypt and Decrypt filters allow to apply various encryption/decryption algorithms to the
uploaded file (concrete algorithm is attached by specifying the certain adapter). The LowerCase
and UpperCase filters are suitable for converting text files to lower- and upper-case, respectively
.
The Rename filter allows to rename and/or move an arbitrary file (not only uploaded file). It uses
the rename() PHP function internally, and thats why it is in general not recommended to use
this filter with uploaded files because of security reasons.
The RenameUpload filter seems to be much more useful than other filters, because it allows to
encapsulate the call of the move_uploaded_file() function and move/rename the uploaded file
from a temporary location to its persistent directory. We will show how to use the RenameUpload
filter in the Image Gallery code example later in this chapter.
10.6.1 FileInput
For storing validation rules for uploaded files, you must use the FileInput class instead of the
usual Input class.
In your form models addInputFilter() private method, you add the validation rules for the
file input as follows:
1
2
3
4
5
6
7
8
9
10
11
$inputFilter->add(array(
'type'
=> 'Zend\InputFilter\FileInput',
'name'
=> 'file', // Element's name.
'required' => true,
// Whether the field is required.
'filters' => array(
// Filters.
// Put filter info here.
),
'validators' => array( // Validators.
// Put validator info here.
)
);
In the authors opinion, the above mentioned four filters are not very useful when working with uploaded files, because you rarely need
to encrypt an uploaded file or convert it to lower case letters.
312
Above, we set the type key (line 2) with the value Zend\InputFilter\FileInput class name.
The rest of keys is analogous to those we used before when adding validation rules for a form
model.
The behaviour of FileInput class differs from the Input in the following aspects:
1. It expects the data you pass as input to be in the $_FILES array format (an array entry
with tmp_name, error, type keys).
2. A Zend\Validator\File\Upload validator is automatically inserted before all other validators into the validator chain of the input.
3. The validators inserted to the validator chain of the input are executed before the filters
inserted into its filter chain. This is opposite to the behaviour of the Input class).
When working with uploaded files, we first need to check that data extracted from $_FILES
super-global array is correct, and then do anything else with the files (moving the file into a
storage directory, renaming it, etc.) Because of that, file validators need to be run first turn, and
filters to be executed last.
To see how this is performed, recall the typical workflow for a form:
First, we call the setData() method to fill in the form with data.
Call the isValid() method to execute filters and validators in the input filter attached to
form.
On successful validation, call the getData() to extract the filtered and validated data from
the input filter attached to form.
On failure, call the getMessages() to retrieve the validation error messages.
When using a FileInput input, the workflow is the same, however it is important to understand
what happens on each of its steps:
Call the setData() method to fill in the form with data.
Call the isValid() method to execute validators in the input filter attached to form.
On successful validation, call the getData() to execute filters and extract the filtered and
validated data from the input filter attached to form.
On failure, call the getMessages() to retrieve the validation error messages.
313
Please note that for FileInput input, the attached filters are only run if the getData()
method is called.
When you use both Input and FileInput inputs in your forms input filter (which is a common
case), the filters are still executed first for usual inputs, but validators are executed first for file
inputs.
<?php
//...
class IndexController extends AbstractActionController
{
// This is the "upload" action displaying the Upload page.
public function uploadAction()
{
// Create the form model.
$form = new YourForm();
// Check if user has submitted the form.
if($this->getRequest()->isPost()) {
// Make certain to merge the files info!
$request = $this->getRequest();
$data = array_merge_recursive(
$request->getPost()->toArray(),
$request->getFiles()->toArray()
);
// Pass data to form.
$form->setData($data);
// Execute file validators.
if($form->isValid()) {
// Execute file filters.
$data = $form->getData();
// Redirect the user to another page.
31
32
33
34
35
36
37
38
39
40
41
314
return $this->redirect()->toRoute('application/default',
array('controller'=>'index', 'action'=>'index'));
}
}
// Render the page.
return new ViewModel(array(
'form' => $form
));
}
}
As you can see from the code above, the uploadAction() looks like a usual controller action
implementing a typical form workflow, however it has some aspects specific to file uploads
(marked with bold):
In line 9, we create an instance of the ImageForm form model with the help of the new
operator.
In line 12, we check whether the request is an HTTP POST request. If so, we get the data
from $_POST and $_FILES super-global PHP arrays and merge them into the single
array (lines 15-19). This is required to correctly handle uploaded files, if any. Then
we pass this array to the form model with the setData() method (line 22).
In line 25, we call the form models isValid() method. This method runs the input filter
attached to the form model. For FileInput inputs, this will execute attached validators
only.
If the data is valid, we call the getData() method (line 28). For the FileInput inputs, this
will run the attached file filters. The file filters, for example, could move the uploaded
files to the directory of residence.
On success, in line 31, we redirect the user to the index action of the controller.
In the controller action above, you should remember three things: 1) merge $_POST and
$_FILES super-global arrays before you pass them to the forms setData() method; 2)
use isValid() forms method to check uploaded files for correctness (run validators);
3) use getData() forms method to run file filters.
315
316
<?php
namespace Application\Form;
use Zend\Form\Form;
// This form is used for uploading an image file.
class ImageForm extends Form
{
// Constructor.
public function __construct()
{
// Define form name.
parent::__construct('image-form');
// Set POST method for this form.
$this->setAttribute('method', 'post');
// Set binary content encoding.
$this->setAttribute('enctype', 'multipart/form-data');
$this->addElements();
}
// This method adds elements to form.
protected function addElements() {
// Add "file" field.
$this->add(array(
'type' => 'file',
'name' => 'file',
'attributes' => array(
'id' => 'file'
),
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
317
We have already discussed the form model creation and the code above should not cause any
problems in its understanding. We just want to attract the attention of the reader that in line 19,
we set the multipart/form-data value for the enctype attribute of the form to make the form
use binary encoding for its data.
Actually, explicitly setting the enctype attribute in forms constructor is optional,
because Zend\Form\Element\File element performs that automatically when you call
forms prepare() method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
namespace Application\Form;
use Zend\InputFilter\InputFilter;
// This form is used for uploading an image file.
class ImageForm extends Form
{
// Constructor
public function __construct()
{
// ...
// Add validation rules
$this->addInputFilter();
}
// ...
// This method creates input filter (used for form filtering/validation).
private function addInputFilter() {
$inputFilter = new InputFilter();
$this->setInputFilter($inputFilter);
// Add validation rules for the "file" field.
$inputFilter->add(array(
'type'
=> 'Zend\InputFilter\FileInput',
'name'
=> 'file',
'required' => true,
'validators' => array(
array(
'name'
=> 'FileUploadFile',
),
array(
'name'
=> 'FileIsImage',
),
array(
'name'
=> 'FileImageSize',
'options' => array(
'minWidth' => 128,
'minHeight' => 128,
'maxWidth' => 4096,
'maxHeight' => 4096
)
),
318
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
319
),
'filters' => array(
array(
'name' => 'FileRenameUpload',
'options' => array(
'target'=>'./data/upload',
'useUploadName'=>true,
'useUploadExtension'=>true,
'overwrite'=>true,
'randomize'=>false
)
)
),
)
);
}
}
320
class ImageManager and put it to Application\Service namespace. We will also register this
service in the service manager component of the web application.
The ImageManager service class will have the following public methods (listed in table 10.3):
Table 10.3. Public methods of the ImageManager class.
Method
Description
getSaveToDir()
getSavedFiles()
getImagePathByName($fileName)
getImageFileInfo($filePath)
getImageFileContent($filePath)
resizeImage($filePath, $desiredWidth)
In fact, we could put the code we plan to add into the service into the controller actions,
but that would make the controller fat and purely testable. By introducing the service
class, we improve the separation of concerns and code reusability.
Add the ImageManager.php file to the Application/Service directory under the modules source
directory. Add the following code to the file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
namespace Application\Service;
// The image manager service.
class ImageManager
{
// The directory where we save image files.
private $saveToDir = './data/upload/';
// Returns path to the directory where we save the image files.
public function getSaveToDir()
{
return $this->saveToDir;
}
}
As you can see from the code above, we define the ImageManager class in line 5. It has the
private $uploadDir property which contains the path to the directory containing our uploaded
files (line 9) (we store uploaded files in APP_DIR/data/upload directory).
Although the ImageManager class is a service and focused on providing services, it can have properties intended for its internal use.
321
The getSaveToDir() public method (line 12) allows to retrieve the path to the upload directory.
Next, we want to add the getSavedFiles() public method to the service class. The method will
scan the upload directory and return an array containing the names of the uploaded files. To add
the getSavedFiles() method, modify the code in the following way
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
//...
// The image manager service.
class ImageManager
{
//...
// Returns the array of uploaded file names.
public function getSavedFiles()
{
// The directory where we plan to save uploaded files.
// Check whether the directory already exists, and if not,
// create the directory.
if(!is_dir($this->saveToDir)) {
if(!mkdir($this->saveToDir)) {
throw new \Exception('Could not create directory for uploads: ' .
error_get_last());
}
}
// Scan the directory and create the list of uploaded files.
$files = array();
$handle = opendir($this->saveToDir);
while (false !== ($entry = readdir($handle))) {
if($entry=='.' || $entry=='..')
continue; // Skip current dir and parent dir.
$files[] = $entry;
}
// Return the list of uploaded files.
return $files;
}
}
In the getSavedFiles() method above, we first check if the upload directory exists (line 16), and
if not, we try to create it (line 17). Then, we get the list of files in the directory (lines 24-32) and
return it to the caller.
322
Next, we add the three methods for getting information about an uploaded file:
the getImagePathByName() method will take the file name and prepend the path to upload
directory to that file name;
the getImageFileInfo() method will retrieve MIME information about the file and its
size in bytes;
and the getImageFileContent() will read file data and return them as a string.
To add those three methods, change the code as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
//...
// The image manager service.
class ImageManager
{
//...
// Returns the path to the saved image file.
public function getImagePathByName($fileName)
{
// Take some precautions to make file name secure.
str_replace("/", "", $fileName); // Remove slashes.
str_replace("\\", "", $fileName); // Remove back-slashes.
// Return concatenated directory name and file name.
return $this->saveToDir . $fileName;
}
// Returns the image file content. On error, returns boolean false.
public function getImageFileContent($filePath)
{
return file_get_contents($filePath);
}
// Retrieves the file information (size, MIME type) by image path.
public function getImageFileInfo($filePath)
{
// Try to open file
if (!is_readable($filePath)) {
return false;
}
// Get file size in bytes.
$fileSize = filesize($filePath);
36
37
38
39
40
41
42
43
44
45
46
47
48
323
Finally, we want to add the image resizing functionality to the ImageManager class. The image
resizing functionality will be used for creating small thumbnail images. Add the resizeImage()
method to the ImageManager class as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
//...
class ImageManager
{
//...
// Resizes the image, keeping its aspect ratio.
public function resizeImage($filePath, $desiredWidth = 240)
{
// Get original image dimensions.
list($originalWidth, $originalHeight) = getimagesize($filePath);
// Calculate aspect ratio
$aspectRatio = $originalWidth/$originalHeight;
// Calculate the resulting height
$desiredHeight = $desiredWidth/$aspectRatio;
// Resize the image
$resultingImage = imagecreatetruecolor($desiredWidth, $desiredHeight);
$originalImage = imagecreatefromjpeg($filePath);
imagecopyresampled($resultingImage, $originalImage, 0, 0, 0, 0,
$desiredWidth, $desiredHeight, $originalWidth, $originalHeight);
// Save the resized image to temporary location
$tmpFileName = tempnam("/tmp", "FOO");
imagejpeg($resultingImage, $tmpFileName, 80);
// Return the path to resulting image.
324
29
30
31
return $tmpFileName;
}
}
The resizeImage() method above takes two arguments: $filePath (the path to the image file),
and $desiredWidth (the width of the thumbnail image). Inside the method, we first calculate an
appropriate thumbnail image height (lines 11-16) preserving its aspect ratio. Then, we resize the
original image as needed and save it to a temporary file (lines 19-26).
As the ImageManager class is ready, you have to register the ImageManager service in the
service manager component of the web application by adding the following lines to the
module.config.php configuration file:
<?php
return array(
// ...
'service_manager' => array(
// ...
'invokables' => array(
// Register the ImageManager service
'ImageManager'=>'Application\Service\ImageManager',
),
),
// ...
);
By doing this, you will be able to get a singleton instance of the service in any controller of your
web application as shown in the code example below:
$imageManager = $this->getServiceLocator()->get('ImageManager');
Action Method
Description
ImageController:uploadAction()
ImageController:indexAction()
ImageController:fileAction()
325
To start, create the ImageController.php file in the Application/Controller directory under the
modules source directory. Put the following stub code into the file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Application\Form\ImageForm;
// This controller is designed for managing image file uploads.
class ImageController extends AbstractActionController
{
// This is the default "index" action of the controller. It displays the
// Image Gallery page which contains the list of uploaded images.
public function indexAction()
{
}
// This action shows the image upload form. This page allows to upload
// a single file.
public function uploadAction()
{
}
// This is the 'file' action that is invoked when a user wants to
// open the image file in a web browser or generate a thumbnail.
public function fileAction()
{
}
}
In the code above, we defined the ImageController class living in the Application\Controller
namespace and added three action method stubs into the class: indexAction() (line 13),
uploadAction() (line 19) and fileAction() (line 25). Next, we will populate those action
methods with the code.
10.8.4.1 Adding Upload Action & Corresponding View Template
First, we will complete the uploadAction() method of our controller. This action method will
handle the Upload a New Image web page containing the upload form. The form will provide an
ability to upload an image file to the gallery.
Change the ImageController.php file as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
326
<?php
//...
class ImageController extends AbstractActionController
{
//...
public function uploadAction()
{
// Create the form model.
$form = new ImageForm();
// Check if user has submitted the form.
if($this->getRequest()->isPost()) {
// Make certain to merge the files info!
$request = $this->getRequest();
$data = array_merge_recursive(
$request->getPost()->toArray(),
$request->getFiles()->toArray()
);
// Pass data to form.
$form->setData($data);
// Validate form.
if($form->isValid()) {
// Move uploaded file to its destination directory.
$data = $form->getData();
// Redirect the user to "Image Gallery" page.
return $this->redirect()->toRoute('application/default',
array('controller'=>'image', 'action'=>'index'));
}
}
// Render the page.
return new ViewModel(array(
'form' => $form
));
}
}
327
$_POST and $_FILES super-global PHP arrays and merge them into the single array (lines 15-19).
This is required to correctly handle uploaded files, if any. Then we pass this array to the form
model with the setData() method (line 22).
In line 25, we call the form models isValid() method. This method runs the input filter attached
to the form model. Since we have only one file input in the input filter, this will only run our
three file validators: UploadFile, IsImage and ImageSize.
If the data is valid, we call the getData() method (line 28). For our file field, this will run the
RenameUpload filter, which moves our uploaded file to its persistent directory.
After that, in line 31, we redirect the user to the index action of the controller (we will populate
that action method a little bit later.
Now, its time to add the view template for the upload action. Add the upload.html view
template file under the application/image directory under the modules view directory:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
$form = $this->form;
$form->get('submit')->setAttributes(array('class'=>'btn btn-primary'));
$form->prepare();
?>
<h1>Upload a New Image</h1>
<p>
Please fill out the following form and press the <i>Upload</i> button.
</p>
<div class="row">
<div class="col-md-6">
<?php echo $this->form()->openTag($form); ?>
<div class="form-group">
<?php echo $this->formLabel($form->get('file')); ?>
<?php echo $this->formElement($form->get('file')); ?>
<?php echo $this->formElementErrors($form->get('file')); ?>
</div>
<?php echo $this->formElement($form->get('submit')); ?>
<?php echo $this->form()->closeTag(); ?>
</div>
</div>
In the code of the view template, we first set class attribute on the form (line 3). This is to apply
nice-looking Twitter Bootstrap styles to the forms Submit button.
328
Then, we render the form with the common view helpers that we discussed in Chapter 7. For
rendering the file field, we use the generic FormElement view helper.
Typically, you use the FormElement generic view helper for rendering the file field.
The FormElement internally calls the FormFile view helper, which performs the actual
rendering.
<?php
//...
class ImageController extends AbstractActionController
{
//...
public function indexAction()
{
// Get the singleton of the image manager service.
$imageManager = $this->getServiceLocator()->get('ImageManager');
// Get the list of already saved files.
$files = $imageManager->getSavedFiles();
// Render the view template.
return new ViewModel(array(
'files'=>$files
));
}
}
In the code above, we first get the singleton instance of the ImageManager service (line 9). We
do that with the getServiceLocator() method provided by the controllers base class. The
getServiceLocator() method returns the ServiceLocatorInterface interface, which provides
an ability to retrieve any service registered in the service manager.
Next, in line 12, we use the getSavedFiles() method of the ImageManager class for retrieving
the list of uploaded images and pass them to the view for rendering (line 15).
Please note how slim and clear this controller action is! We achieved this by moving
the image management functionality to the ImageManager service model.
329
Add the index.phtml view template to image/index directory under the modules view directory.
The contents of the file is shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<h1>Image Gallery</h1>
<p>
This page displays the list of uploaded images.
</p>
<p>
<a href="<?php echo $this->url('application/default',
array('controller'=>'image', 'action'=>'upload')); ?>"
class="btn btn-primary" role="button">Upload More</a>
</p>
<hr/>
<?php if(count($files)==0): ?>
<p>
<i>There are no files to display.</i>
</p>
<?php else: ?>
<div class="row">
<div class="col-sm-6 col-md-12">
<?php foreach($files as $file): ?>
<div class="img-thumbnail">
<img src="<?php echo $this->url('application/default',
array('controller'=>'image', 'action'=>'file'),
array('query'=>array('name'=>$file, 'thumbnail'=>true))); ?>">
<div class="caption">
<h3><?php echo $file; ?></h3>
<p>
<a target="_blank" href="<?php echo $this->url('application/default',\
array('controller'=>'image', 'action'=>'file'),
array('query'=>array('name'=>$file))); ?>"
class="btn btn-default" role="button">Show in Natural Size</a>
</p>
</div>
44
45
46
47
48
49
50
51
52
330
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<hr/>
In the code above, we create the HTML markup for the Upload More button (line 5).
Under the button, we use check whether the $files array is empty (line 10). If the array is empty,
we output the There are no files to display message; otherwise we walk through the files and
output the thumbnails of each uploaded images.
For rendering a thumbnail, we use the <img> tag. We set its src attribute with the URL pointing
to the file action of our ImageController controller. We pass two parameters to the action via
the query part of the URL: the image name and thumbnail flag.
For styling the thumbnails, we use the Twitter Bootstrap provided .img-thumbnail CSS class.
For additional information about these Twitter Bootstrap styles, please refer to the
Bootstrap official documentation.
Below each thumbnail, we put the Show in Natural Size link, which points to the file action
of our ImageController controller. When site visitor clicks the link, he will be shown with the
image in natural size, and the image will be opened in another browsers tab (note the target="_blank" attribute of the link).
10.8.4.3 Adding File Action
The last action we will populate is the ImageController::fileAction() method. That method
will allow to preview an uploaded image or generate a small thumbnail of the image. The action
method will take two GET parameters:
the name parameter defines the file name for preview;
the thumbnail parameter is a flag telling whether we want to dump the full image or its
small copy.
Change the ImageController.php file as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
//...
class ImageController extends AbstractActionController
{
//...
public function fileAction()
{
// Get the file name from GET variable.
$fileName = $this->params()->fromQuery('name', '');
// Check whether the user needs a thumbnail or a full-size image.
$isThumbnail = (bool)$this->params()->fromQuery('thumbnail', false);
// Get the singleton of the image manager service.
$imageManager = $this->getServiceLocator()->get('ImageManager');
// Get path to image file.
$fileName = $imageManager->getImagePathByName($fileName);
if($isThumbnail) {
// Resize the image.
$fileName = $imageManager->resizeImage($fileName);
}
// Get image file info (size and MIME type).
$fileInfo = $imageManager->getImageFileInfo($fileName);
if ($fileInfo===false) {
// Set 404 Not Found status code
$this->getResponse()->setStatusCode(404);
return;
}
// Write HTTP headers.
$response = $this->getResponse();
$headers = $response->getHeaders();
$headers->addHeaderLine("Content-type: " . $fileInfo['type']);
$headers->addHeaderLine("Content-length: " . $fileInfo['size']);
// Write file content.
$fileContent = $imageManager->getImageFileContent($fileName);
if($fileContent!==false) {
$response->setContent($fileContent);
} else {
// Set 500 Server Error status code.
$this->getResponse()->setStatusCode(500);
331
47
48
49
50
51
52
53
54
55
56
57
58
332
return;
}
if($isThumbnail) {
// Remove temporary thumbnail image file.
unlink($fileName);
}
// Return Response to avoid default view rendering.
return $this->getResponse();
}
}
In the code above, we first get the name and thumbnail parameters from $_GET super-global
array (lines 9, 12). If the parameters are missing, their default values are used instead.
Then, in line 15, we retrieve a singleton instance of our ImageManager service that we previously
registered in the service manager.
In line 18, we use the getImagePathByName() method provided by the ImageManager service to
get the absolute path to the image by its name.
If a thumbnail is requested, we resize the image with the resizeImage() method of the
ImageManager (line 23). That method returns path to a temporary file containing the thumbnail
image.
Then, we get the information about the image file (its MIME type and file size) with the
getImageFileInfo() method of the ImageManager (line 27).
Finally, we create a Response object, fill its headers with image information, set its content with
data of the image file (lines 35-48), and return the Response object from the controller action
(line 56).
Note that returning the Response object disables the default rendering of the view
template for this action method. By this reason, we do not create the file.phtml view
template file.
333
1
2
3
4
5
6
7
8
9
10
11
12
<?php
return array(
//...
'controllers' => array(
'invokables' => array(
'Application\Controller\Image' =>
'Application\Controller\ImageController',
//...
),
),
//...
);
10.8.5 Results
Finally, adjust directory permissions to make the APP_DIR/data directory writeable by the
Apache Web Server. In Linux Ubuntu, this is typically accomplished by the following shell
commands (replace the APP_DIR placeholder with the actual directory name of your web
application):
chown -R www-data:www-data APP_DIR/data
chmod -R 775 APP_DIR/data
Above, the chown and chmod commands set the Apache user to be the owner of the directory and
allow the web server to write to the directory, respectively.
If you now enter the URL http://localhost/application/image/index into your web browsers
navigation bar, you will see the image gallery page like shown in figure 10.4.
334
Clicking the Upload More button will open the Upload a New Image page where you can peek
an image file for upload. If you pick an unacceptable file (not an image, or too big image), you
will see validation errors (see the figure 10.5 below).
If the upload is completed successfully, you will be redirected back to the Image Gallery page
and see the uploaded image in the list of thumbnails. Clicking the View Full Size button will open
the image in a new browser tab (see the figure 10.6 below for example).
335
You may find the Image Gallery complete example in the Form Demo sample web
application bundled with this book.
10.9 Summary
File uploads is a standard HTML form feature. Uploading files is accomplished by setting form
content encoding to binary encoding type. Zend Framework 2 provides convenient functionality
for doing file uploads and validating the uploaded files.
Description
Zend\Captcha
Zend\Form
Zend\Filter
Zend\Validator
Zend\InputFilter
11.1.1 CAPTCHA
A CAPTCHA (stands for Completely Automated Public Turing test to tell Computers and
Humans Apart) is a challenge-response test used in web sites for determining whether the user
is a human or a robot.
There are several types of CAPTCHA. The most widely used one requires that the user type the
letters of a distorted image that is shown on the web page (see figure 11.1 for some examples).
337
interfaces to work together. Typically, CAPTCHA algorithms have different public methods, but since they all implement AbstractAdapter
interface, the caller may use any CAPTCHA algorithm in the same common manner (by calling the methods provided by the base interface).
338
The Captcha element class can be used with any CAPTCHA algorithm (listed in the previous
section) from Zend\Captcha component. For this purpose, the element class has the setCaptcha()
method which takes either an instance of a class implementing Zend\Captcha\AdapterInterface
interface, or an array containing CAPTCHA configuration . By the setCaptcha() method, you
can attach the desired CAPTCHA type to the element.
You add the Captcha element to a form model as usual, with the add() method provided by the
Zend\Form\Form base class. As usual, you can pass it either an instance of the Zend\Form\Element\Captcha
class or provide an array of configuration options specific to certain CAPTCHA algorithm (in
that case, the element and its associated CAPTCHA algorithm will automatically be instantiated
and configured by the factory class).
The code example below shows how to use the latter method (passing a configuration array).
We prefer this method because it requires less code to write. It is assumed that you call this code
inside of form models addElements() protected method:
In the latter case (configuration array), the CAPTCHA algorithm will be automatically instantiated and initialized by the factory class
Zend\Captcha\Factory.
1
2
3
4
5
6
7
8
9
10
11
12
13
339
<?php
// Add the CAPTCHA field to the form model
$this->add(array(
'type' => 'captcha',
'name' => 'captcha',
'options' => array(
'label' => 'Human check',
'captcha' => array(
'class' => '<captcha_class_name>', //
// Certain-class-specific options follow here ...
),
),
));
In the example above, we call the add() method provided by the Form base class and pass it an
array describing the element to insert (line 3):
The type key of the array (line 4), as usual, may either be a full name of the element
(Zend\Form\Element\Captcha) or its short alias (captcha).
The name key (line 5) is the value for the name attribute of the HTML form field.
The options key contains the options for the attached CAPTCHA algorithm. The class
key (line 9) may either contain the full CAPTCHA class name (e.g. Zend\Captcha\Image)
or its short alias (e.g. Image). Other, adapter-specific, options may be added to the key
as well. We will show how to do that a little bit later.
For generating the HTML markup for the element, you may use the FormCaptcha view helper
class (belonging to Zend\Form\View\Helper namespace). But, as you might learn from the
previous chapter, typically you use the generic FormElement view helper instead, like shown
in the code below:
<?php echo $this->formElement($form->get('captcha')); ?>
It is assumed that you call the view helper inside of your view template.
Next, we provide three examples illustrating how to use different CAPTCHA types provided
by ZF2: the Image, Figlet and ReCaptcha. We will show how to add a CAPTCHA field to the
feedback form that we used in examples of the previous chapter.
To add the Image CAPTCHA to your form model, call the forms add() method as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
340
<?php
namespace Application\Form;
// ...
class ContactForm extends Form
{
// ...
protected function addElements() {
// ...
// Add the CAPTCHA field
$this->add(array(
'type' => 'captcha',
'name' => 'captcha',
'attributes' => array(
),
'options' => array(
'label' => 'Human check',
'captcha' => array(
'class' => 'Image',
'imgDir' => 'public/img/captcha',
'suffix' => '.png',
'imgUrl' => '/img/captcha/',
'imgAlt' => 'CAPTCHA Image',
'font'
=> './data/font/thorne_shaded.ttf',
'fsize' => 24,
'width' => 350,
'height' => 100,
'expiration' => 600,
'dotNoiseLevel' => 40,
'lineNoiseLevel' => 3
),
),
));
}
}
Above, the captcha key of the configuration array (see line 20) contains the following parameters
for configuring the Image CAPTCHA algorithm attached to the form element:
the class parameter (line 21) should be either the full CAPTCHA adapter class name
(\Zend\Captcha\Image) or its short alias (Image).
the imgDir parameter (line 22) should be the path to the directory where to save the
generated distorted images (in this example, we will save the images to the APP_DIR/public/img/captcha directory).
341
the suffix parameter (line 23) defines the extension for a generated image file (.png in
this example).
the imgUrl parameter (line 24) defines the base part of the URL for opening generated
CAPTCHA images in a web browser. In this example, site visitors will be able to access
CAPTCHA images using URLs like http://localhost/img/captcha/<ID>, where ID is a
unique ID of certain image.
the imgAlt parameter (line 25) is an (optional) alternative text to show if CAPTCHA image
cant be loaded by the web browser (the alt attribute of <img> tag).
the font parameter (line 26) is the path to the font file. You can download a free TTF
font, for example, from here. In this example, we use Thorne Shaded font, which we
downloaded and put into the APP_DIR/data/font/thorne_shaded.ttf file.
the fsize parameter (line 27) is a positive integer number defining the font size.
the width (line 28) and height parameters (line 29) define the with and height (in pixels)
of the generated image, respectively.
the expiration parameter (line 30) defines the expiration period (in seconds) of the
CAPTCHA images. Once an image expires, it is removed from disk.
the dotNoiseLevel parameter (line 31) and lineNoiseLevel parameter (line 32) define the
image generation options (dot noise level and line noise level, respectively).
To render the CAPTCHA field, add the following lines to your contact-us.phtml view template
file:
<div class="form-group">
<?php echo $this->formLabel($form->get('captcha')); ?>
<?php echo $this->formElement($form->get('captcha')); ?>
<?php echo $this->formElementErrors($form->get('captcha')); ?>
<p class="help-block">Enter the letters above as you see them.</p>
</div>
Finally, create the APP_DIR/public/img/captcha directory that will store generated CAPTCHA
images. Adjust directory permissions to make the directory writeable by the Apache Web Server.
In Linux Ubuntu, this is typically accomplished by the following shell commands (replace the
APP_DIR placeholder with the actual directory name of your web application):
mkdir APP_DIR/public/img/captcha
chown -R www-data:www-data APP_DIR
chmod -R 775 APP_DIR
Above, the mkdir command creates the directory, and chown and chmod commands set the
Apache user to be the owner of the directory and allow the web server to write to the directory,
respectively.
Now, if you open the http://localhost/contactus page in your web browser, the CAPTCHA
image will be generated based on a random sequence of letters and digits saved in session. You
should see something like in the figure 11.3 below.
http://www.1001freefonts.com/
342
When you fill the form fields in and press the Submit button, the letters entered into the Human
check field will be transferred to server as part of HTTP request. Then, on form validation, the
Zend\Form\Element\Captcha class will compare the submitted letters to those stored in PHP
session. If the letters are identical, the form is considered valid; otherwise form validation fails.
Once the PHP renderer processes the view template, it generates HTML markup for the
CAPTCHA element as shown below:
<div class="form-group">
<label for="captcha">Human check</label>
<img width="350" height="100" alt="CAPTCHA Image"
src="/img/captcha/df344b37500dcbb0c4d32f7351a65574.png">
<input name="captcha[id]" type="hidden"
value="df344b37500dcbb0c4d32f7351a65574">
<input name="captcha[input]" type="text">
<p class="help-block">Enter the letters above as you see them.</p>
</div>
343
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
// Add the CAPTCHA field
$this->add(array(
'type' => 'captcha',
'name' => 'captcha',
'attributes' => array(
),
'options' => array(
'label' => 'Human check',
'captcha' => array(
'class' => 'Figlet',
'wordLen' => 6,
'expiration' => 600,
),
),
));
Above, the captcha key of the configuration array (see line 9) contains the following parameters
for configuring the Figlet CAPTCHA algorithm attached to the form element:
the class parameter (line 10) should be either the full CAPTCHA adapter class name
(\Zend\Captcha\Figlet) or its short alias (Figlet).
the wordLen parameter (line 11) defines the length of the secret word to be generated.
the expiration parameter (line 12) defines the CAPTCHA expiration period (in seconds).
Now, open the http://localhost/contactus page in your web browser. Once that is done, you
should see a page like in the figure 11.4 below.
Once the PHP renderer processes the view template, it generates HTML markup for the
CAPTCHA element like shown below:
<div class="form-group">
<label for="captcha">Human check</label>
<pre>
__
_
__
__
_
_
___
_
_
| || | || \ \\/ // | \ / ||
/ _ \\ | || | ||
| '--' ||
\ ` // | \/ || | / \ || | || | ||
| .--. ||
| ||
| . . || | \_/ || | \\_/ ||
|_|| |_||
|_||
|_|\/|_||
\___//
\____//
`-` `-`
`-`'
`-` `-`
`---`
`---`
__
__
\ \\/ //
\ ` //
| ||
|_||
`-`'
</pre>
<input name="captcha[id]" type="hidden"
value="b68b010eccc22e78969764461be62714">
<input name="captcha[input]" type="text">
<p class="help-block">Enter the letters above as you see them.</p>
</div>
344
345
The commands above make your sites directory to be the current working directory and run
Composer dependency manager to install the component.
Next, you have to register on recaptcha.net web-site (figure 10.5) and get the public and private
keys from there.
To add the reCAPTCHA element to your form model, replace the form element definition from
the previous example with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
346
15
16
17
),
),
));
Above, the captcha key of the configuration array (see line 9) contains the following parameters
for configuring the ReCaptcha CAPTCHA algorithm attached to the form element:
the class parameter (line 10) should be either the full CAPTCHA adapter class name
(\Zend\Captcha\ReCaptcha) or its short alias (ReCaptcha).
the privKey parameter (line 12) should be the public key youve got from recaptcha.net.
the pubKey parameter (line 14) should be the private key youve got from recaptcha.net.
Please note, that in code above, you need to set the privKey parameter with the value
of the public key acquired from recaptcha.net, and the pubKey parameter with the value
of the private key. Exactly in this order; otherwise it will not work.
Now, if you open the http://localhost/contactus page in your web browser, you should see
something like in the figure 11.6 below.
347
able to send unauthorized commands from a user that the web-site trusts. This attack is typically
performed on pages containing forms for submission of some sensitive data (e.g. money transfer
forms, shopping carts etc.)
To better understand how this attack works, take a look at figure 11.7.
348
The above described CSRF attack is possible it the web form on the payment gateway
site does not check the source of the HTTP request. The people who maintain the
payment gateway site must put more attention in making its forms more secure.
To prevent CSRF attacks to a form, one has to require a special token with the form, as follows:
1. For certain form, generate a random sequence of bytes (token) and save it server-side in
PHP session data.
2. Add a hidden field to form and set its value with the token.
3. Once the form is submitted by the user, compare the hidden value passed in the form with
the token saved server-side. If they match, consider the form data secure.
If a malicious user will try to attack the site by submitting the form, he will not be able
to put right token in the form submissions, because the token is not stored in cookies.
To insert a CSRF element to your form model, add the following lines in its addElements()
method:
1
2
3
4
5
6
7
8
9
10
Above, we use the Forms add() method (line 2), to which we pass a configuration array
describing the CSRF element. The element will be automatically instantiated and initialized by
the factory.
In line 3, we specify the class name for the CSRF element. This either may be the full class name
(Zend\Form\Element\Csrf) or a short alias (csrf).
349
In line 4, we set the name attribute for the element. In this example, we use csrf name, but
you may use any other name, on your choice.
In line 6, inside of csrf_options array, we specify the options specific to Zend\Form\Element\Csrf
class. We set the timeout option to 600 (look at line 7), which means the CSRF check expires in
600 seconds (10 minutes) after form creation.
To render the CSRF field, in your view template .phtml file , add the following line:
<?php echo $this->formElement($form->get('csrf')); ?>
When the PHP renderer evaluates the view template, it generates the HTML markup for the
CSRF field like shown below:
<input type="hidden" name="csrf" value="1bc42bd0da4800fb55d16e81136fe177">
As you can see from the HTML markup code above, the form now contains a hidden
field with a randomly generated token. Since the attacker script doesnt know this
token, it wont be able to submit its correct value, thus the CSRF attack becomes
prevented.
11.2 Summary
In this chapter, we have discussed some advanced form usage capabilities.
Zend Framework 2 provides two classes whose purpose is enhancing form security: Captcha and
Csrf. A CAPTCHA is a type of challenge-response test used to determine whether or not the user
is a human. CAPTCHA elements are used on form to prevent form submission by a malicious
automated process (a robot). The latter element, Csrf, is used for Cross-Site Request Forgery
(abbreviated as CSRF) hacker attack prevention.
351
To download the Blog application, visit this page and click the Download ZIP button to download
the code as a ZIP archive. When download is complete, unpack the archive to some directory.
Then navigate to the blog directory containing the source code of the Blog web application:
/using-zend-framework-2-book
/blog
...
The Blog is a sample web site which can be installed on your machine. To install the example,
you can either edit your default Apache virtual host file or create a new one. After editing the
file, restart the Apache HTTP Server and open the web site in your web browser.
For the Blog example to work, you have to create a MySQL database. Instructions on
how to do that are provided in the next section.
https://github.com/olegkrivtsov/using-zend-framework-2-book
352
Once you install MySQL, type the following command from your command shell to log into
MySQL client console:
mysql -u root -p
When asked for, type the password of the root user (the password of the root user is the one
youve specified during MySQL server installation). On successful login, you should see the
following welcome message:
353
Now you are able to type MySQL client commands (like show databases, show tables, etc.) or
SQL queries (like SELECT or INSERT) in the MySQL prompt and see their output.
If you want to quit of the MySQL prompt, type quit and press Enter.
MySQL commands are case insensitive, so you could type create schema blog;
with the same result. We recommend using upper case for SQL queries, since this is a
common convention.
Next, we create the user named blog and grant it all privileges for accessing and modifying the
blog database and all its tables:
GRANT ALL PRIVILEGES ON blog.* TO `blog`@`localhost` IDENTIFIED BY '<passwd>';
In the command above, replace the password placeholder with the new password for the blog
user. This password should be different than the password of the root user.
354
Here, we create the second user blog, because it is not recommended to give the web
application to log into database under the root user. The root user has unlimited rights
and it would be just insecure to give the application an ability to do any actions it
wants. The blog user will have permissions to modify the blog database only, which
is sufficient in our case.
You can check that the database has been created by typing the following command and pressing
Enter:
show databases;
You should be able to see the output like below (note the blog line in the list of databases):
+--------------------+
| Database
|
+--------------------+
| information_schema |
| blog
|
| mysql
|
| performance_schema |
+--------------------+
MySQL client allows to enter multi-line commands easily. Just press Enter when you
want to move the caret to the next line. The command is considered to be fully entered
when the semicolon (;) character is encountered.
355
Lets fill the tables we have created with some sample data:
INSERT INTO tag(`name`) VALUES('zf2');
INSERT INTO tag(`name`) VALUES('book');
INSERT INTO tag(`name`) VALUES('magento');
INSERT INTO post(`title`, `content`, `status`, `date_created`) VALUES(
'Top 10+ Books about Zend Framework 2',
'Post content', 2, '2014-08-09 18:49');
INSERT INTO post(`title`, `content`, `status`, `date_created`) VALUES(
'Getting Started with Magento Extension Development Book Review',
'Post content 2', 2, '2014-08-09 18:51');
INSERT
INSERT
INSERT
INSERT
INTO
INTO
INTO
INTO
post_tag(`post_id`,
post_tag(`post_id`,
post_tag(`post_id`,
post_tag(`post_id`,
`tag_id`)
`tag_id`)
`tag_id`)
`tag_id`)
VALUES(1,
VALUES(1,
VALUES(2,
VALUES(2,
1);
2);
2);
3);
356
If necessary, you can easily remove the schema and all tables and data it contains by
typing the following command from MySQL prompt:
DROP SCHEMA blog;
Figure 12.3 graphically illustrates what entities we have in the schema and what relations
between those entities present.
As you can see from figure 12.3, the post table is related to comment table as one-to-many, because
a single post may have many comments. This is also called the one-to-many relation.
The post table is also related to the tag table as many-to-many. A single post may have many
tags, and a single tag may belong to many posts as well. Many-to-many relation is typically
implemented through an auxiliary table (post_tag table in our case).
357
APP_DIR/data/ directory and type the following command from your command shell (but not
from MySQL prompt):
mysql -uroot -p blog < schema.mysql.sql
When prompted for password, enter the password of the root user and type Enter.
Once this is done, log into MySQL client and type the following commands:
use blog;
show tables;
You should see the list of tables created, something like below:
+----------------+
| Tables_in_blog |
+----------------+
| comment
|
| post
|
| post_tag
|
| tag
|
+----------------+
4 rows in set (0.00 sec)
358
The cd command above is used to make the APP_DIR directory current working directory.
And the require command tells Composer to add the package doctrine/doctrine-orm-module
as a dependency to your web application, and to download and install that dependency. The
asterisk (*) parameter means that any version of the package is acceptable.
Specifying the asterisk as a version, will result in installing the latest available version
of Doctrine, which typically is the desired behavior.
Once you run the commands above, Composer will first modify the composer.json file and create
the following line under its require key:
{
...
"require": {
"doctrine/doctrine-orm-module": "*",
...
},
...
}
Then Composer will try to locate the dependency packages, download them to the local machine
and install the files into the APP_DIR/vendor directory.
Composer will output lines indicating installation process to the terminal:
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
- Installing doctrine/lexer (v1.0)
Downloading: 100%
- Installing doctrine/annotations (v1.1.2)
Downloading: 100%
359
As you can see from the output above, when you install DoctrineORMModule component,
Composer automatically installs the DoctrineModule and all necessary Doctrine components
(Doctrine\DBAL, Doctrine\ORM, etc.)
360
When the installation has been finished, you can find the Doctrine files in your APP_DIR/vendor
directory (see the figure 12.4 below).
You use the php composer.phar require command for the first time you install
Doctrine. Once the composer.json (and composer.lock) files have been modified by
Composer, you are able to install (or update) all dependencies as usual by typing the
php composer.phar install or php composer.phar update commands, respectively,
from your command shell.
<?php
return array(
'modules' => array(
'Application',
// Add the Doctrine integration modules.
'DoctrineModule',
'DoctrineORMModule',
),
//...
);
361
The lines above let ZF2 know that it should load the DoctrineModule module and DoctrineORMModule module on applications start up (we talked about ZF2 application life cycle in Chapter
3).
By default, there is only one connection named orm_default, and you may add more
database connections if required.
the configuration key contains ORM settings like caching configuration and locations of
auto-generated entity proxy classes for each available connection.
the driver key contains the information about where to locate entity classes for each
available database connection.
The tree in figure 12.5 may be different than you have in your own application, because some keys were omitted here for simplicity.
362
the entitymanager key contains settings used for instantiating an entity manager for each
database connection.
the eventmanager key contains settings for Doctrine event manager for each available
connection.
363
Doctrine uses its own implementation of event manager. If you want, you can create
an event listener class and hooks some events. However, this advanced topic and we
do not cover it in this book.
364
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
return array(
'doctrine' => array(
'connection' => array(
'orm_default' => array(
'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver',
'params' => array(
'host'
=> '127.0.0.1',
'user'
=> 'blog',
'password' => '<password>',
'dbname'
=> 'blog',
)
),
),
),
);
Above, we have the doctrine key and connection subkey. The connection subkey contains the
orm_default subkey which is the default connection.
The driverClass key provides the class name to use as a driver to the database. Since
we use MySQL database, we specify the Doctrine\DBAL\Driver\PDOMySql\Driver class
name.
For your reference, in table 12.3, you can find several other often used database drivers.
Each driver class supports its own set of parameters, so please refer to certain drivers
code (and related documentation) for additional information.
Method
Description
Doctrine\DBAL\Driver\PDOSqlite\Driver
Doctrine\DBAL\Driver\PDOMySql\Driver
Doctrine\DBAL\Driver\PDOOracle\Driver
Doctrine\DBAL\Driver\PDOPgSql\Driver
Doctrine\DBAL\Driver\PDOSqlsrv\Driver
365
Because the autoload/local.php file contains environment-specific parameters, in version control system you store its distribution template local.php.dist. Each developer
in your team then renames the local.php.dist file into local.php and enters his own
password instead of the placeholder. The local.php file should not be stored under
version control, because you might want that other people in your team (or other people
having access to your code repository) do not see the actual password.
In Doctrine ORM, an entity class is mapped on a certain database table. For example, the User
entity is usually mapped on the user table (if needed, the table name may be arbitrary).
For our Blog example application, we will create three entity classes:
Post entity will contain data related to specific blog post. Its properties are exactly the
same that we used when defining the post table in blog database schema. The entity class
will also have public getter and setter methods designed for retrieving/setting the data.
by analogy, Comment entity will contain data related to a comment to blog post.
and Tag entity will contain data related to a tag.
366
12.5.1 Annotations
An annotation is a special kind of a PHP comment that is preprocessed by Doctrine ORM. In other
words, annotations is metadata attached to an entity class that can be read by the Doctrine ORM
at run-time. Annotations provide verbose information about an entity. Annotations describe an
entity and tell Doctrine ORM how to map it on a database table.
A Docblock annotation is a C++ style comment starting with slash (/) and two asterisks (*).
This starter characters are required, otherwise Doctrine wont recognize the annotation. An
example of annotation can be found below:
1
2
3
/**
* This is Docblock annotation comment.
*/
Doctrine reads Docblock annotations with the help of its Doctrine\Annotations component.
You might have already faced with Docblock annotations if you use phpDocumentor
or Doxygen documentation generation tools. In those tools, annotation comments are
serving the same goal: to describe a PHP class and its properties and methods. Then the
tool goes through your code and builds an HTML documentation automatically based
entirely on code and annotations analysis.
For example, below, we provide the basic example of a Doctrine entity class. You can see that
the class and its properties are marked with Docblock annotations with special tags (a tag starts
with @ character).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="post")
*/
class Post
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(name="id")
*/
http://www.phpdoc.org/
http://www.stack.nl/~dimitri/doxygen/
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
367
protected $id;
/**
* @ORM\Column(name="title")
*/
protected $title;
/**
* @ORM\Column(name="content")
*/
protected $content;
/**
* @ORM\Column(name="status")
*/
protected $status;
/**
* @ORM\Column(name="date_created")
*/
protected $dateCreated;
}
368
@ORM\GeneratedValue is used to tell Doctrine ORM that this property uses some autogenerated sequence for initializing itself (line 15). In MySQL, this typically means that the
corresponding table column uses AUTO_INCREMENT initializer.
@ORM\Column(name="<column_name>") is used to tell Doctrine ORM on which table
column to map this particular property (lines 15, 20, 25, 30, 37).
The complete list of Doctrine-provided tags used in annotations can be found by the
following link.
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* This class represents a single post in a blog.
* @ORM\Entity
* @ORM\Table(name="post")
*/
class Post
{
// Post status constants.
const STATUS_DRAFT
= 1; // Draft.
const STATUS_PUBLISHED
= 2; // Published.
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(name="id")
http://docs.doctrine-project.org/en/2.0.x/reference/annotations-reference.html
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
*/
protected $id;
/**
* @ORM\Column(name="title")
*/
protected $title;
/**
* @ORM\Column(name="content")
*/
protected $content;
/**
* @ORM\Column(name="status")
*/
protected $status;
/*
* @ORM\Column(name="date_created")
*/
protected $dateCreated;
// Returns ID of this post.
public function getId()
{
return $this->id;
}
// Sets ID of this post.
public function setId($id)
{
$this->id = $id;
}
// Returns title.
public function getTitle()
{
return $this->title;
}
// Sets title.
public function setTitle($title)
{
$this->title = $title;
}
369
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
370
// Returns status.
public function getStatus()
{
return $this->status;
}
// Sets status.
public function setStatus($status)
{
$this->status = $status;
}
// Returns post content.
public function getContent()
{
return $this->content;
}
// Sets post content.
public function setContent($content)
{
$this->content = $content;
}
// Returns the date when this post was created.
public function getDateCreated()
{
return $this->dateCreated;
}
// Sets the date when this post was created.
public function setDateCreated($dateCreated)
{
$this->dateCreated = $dateCreated;
}
}
371
Please note that for properties we (by convention) use camel-case names (like
$dateCreated), while for database columns we use canonicalized names (in lowercase and with underscores separating words in a name, like date_created).
Property
Mapped on Column
Description
$id
$title
$content
$status
$dateCreated
id
title
content
status
date_created
Entity class and its properties are marked with Docblock annotations read by Doctrine
ORM at run-time allowing it to know how to map this entity and its properties on the
database table and its columns.
Entity class has getter and setter methods (lines 45-102) allowing to access/modify
the protected properties (see the table 12.5 for reference of methods and their brief
descriptions).
Method
Description
getId()
setId($id)
getTitle()
setTitle($title)
getStatus()
setStatus($status)
getContent()
setContent($content)
getDateCreated()
setDateCreated()
Note that we do not mark entity class methods with Doctrine annotations. There is
just no need to do that. However, you may mark methods with usual comments and
non-Doctrine Docblock annotations, if you strongly wish.
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* This class represents a comment related to a blog post.
* @ORM\Entity
* @ORM\Table(name="comment")
*/
class Comment
{
/**
* @ORM\Id
* @ORM\Column(name="id")
* @ORM\GeneratedValue
*/
protected $id;
/**
* @ORM\Column(name="content")
*/
protected $content;
/**
* @ORM\Column(name="author")
*/
protected $author;
/**
* @ORM\Column(name="date_created")
*/
protected $dateCreated;
// Returns ID of this comment.
public function getId()
{
return $this->id;
}
// Sets ID of this comment.
public function setId($id)
{
$this->id = $id;
372
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
}
// Returns comment text.
public function getContent()
{
return $this->content;
}
// Sets status.
public function setContent($comment)
{
$this->comment = $comment;
}
// Returns author's name.
public function getAuthor()
{
return $this->author;
}
// Sets author's name.
public function setAuthor($author)
{
$this->author = $author;
}
// Returns the date when this comment was created.
public function getDateCreated()
{
return $this->dateCreated;
}
// Sets the date when this comment was created.
public function setDateCreated($dateCreated)
{
$this->dateCreated = $dateCreated;
}
}
Next, create Tag.php file and put the following code inside of it:
373
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* This class represents a tag.
* @ORM\Entity
* @ORM\Table(name="tag")
*/
class Tag
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(name="id")
*/
protected $id;
/**
* @ORM\Column(name="name")
*/
protected $name;
// Returns ID of this tag.
public function getId()
{
return $this->id;
}
// Sets ID of this tag.
public function setId($id)
{
$this->id = $id;
}
// Returns name.
public function getName()
{
return $this->name;
}
// Sets name.
public function setName($name)
{
$this->title = $name;
374
47
48
375
}
}
Since the Comment and Tag entities are analogous to the Post entity, we dont provide detailed
description of the code above.
Please note that we do not create an entity for the fourth auxiliary table post_tag. That
table will be indirectly used further in this chapter when defining relations between
entities.
12.6.3.1 OneToMany/ManyToOne
First, lets define one-to-many relation between the Post and Comment entities. Modify the
Post.php file and add the following lines:
1
2
3
4
5
6
7
8
9
10
11
12
<?php
// ...
use Doctrine\Common\Collections\ArrayCollection;
use Application\Entity\Comment;
/**
* This class represents a single post in a blog.
* @ORM\Entity
* @ORM\Table(name="post")
*/
class Post
{
http://docs.doctrine-project.org/en/2.0.x/reference/association-mapping.html
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
376
// ...
/**
* @ORM\OneToMany(targetEntity="\Application\Entity\Comment", mappedBy="pos\
t")
* @ORM\JoinColumn(name="id", referencedColumnName="post_id")
*/
protected $comments;
/**
* Constructor.
*/
public function __construct()
{
$this->comments = new ArrayCollection();
$this->tags = new ArrayCollection();
}
/**
* Returns comments for this post.
* @return array
*/
public function getComments()
{
return $this->comments;
}
/**
* Adds a new comment to this post.
* @param $comment
*/
public function addComment($comment)
{
$this->comments[] = $comment;
}
}
As you can see from the code above, we added the $comments property (line 20). This property
will be the collection of comments related to certain post.
We initialize the $comments property in class constructor (lines 25-29). By assigning it with a
new instance of Doctrine\Common\Collections\ArrayCollection class.
A Doctrine ArrayCollection is an array of objects, like usual PHP array, but with
additional features required by Doctrine. It is implemented in DoctrineCommon
component.
377
In lines 15-19, we add Doctrine annotations to the $comments property, so Doctrine knows how
to get all comments associated with the post:
the @ORM\OneToMany tag defines that this is the one-to-many relation between the Post
entity and the (target) Comment entity.
the @ORM\JoinColumn tag specifies which column to use for joining the tables associated
with the entities.
The getComments() method (lines 35-37) allows to do get all comments associated with the post.
We also added the addComment() method (lines 44-47) for adding new comment to post. You can
notice that we use the [] operator, just like we do with a typical PHP array.
Vice versa, we define the other side of this relation by modifying the Comment entity as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
// ...
use Doctrine\Common\Collections\ArrayCollection;
// ...
class Comment
{
/**
* @ORM\ManyToOne(targetEntity="Application\Entity\Post", inversedBy="comme\
nts")
* @ORM\JoinColumn(name="post_id", referencedColumnName="id")
*/
protected $post;
/*
* Returns associated post.
* @return \Application\Entity\Post
*/
public function getPost()
{
return $this->post;
}
/**
* Sets associated post.
* @param \Application\Entity\Post $post
*/
public function setPost($post)
{
$this->post = $post;
$post->addComment($this);
}
}
378
In the code above, we added the $post private property to the entity class. This is not a collection,
but a single instance of Post class, because single comment always belongs to single post. The
annotation tags @ORM\ManyToOne and @ORM\JoinColumn are analogous to those we covered before.
12.6.3.2 ManyToMany
Lets now express the many-to-many relation between the Post and Tag entities. For this relation,
we indirectly use the auxiliary post_tag table.
Modify the Post entity as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
//...
use Application\Entity\Tag;
//...
class Post
{
//...
/**
* @ORM\ManyToMany(targetEntity="\Application\Entity\Tag", inversedBy="post\
s")
* @ORM\JoinTable(name="post_tag",
*
joinColumns={@ORM\JoinColumn(name="post_id", referencedColumnName="\
id")},
*
inverseJoinColumns={@ORM\JoinColumn(name="tag_id", referencedColumn\
Name="id")}
*
)
*/
protected $tags;
// Constructor.
public function __construct()
{
//...
$this->tags = new ArrayCollection();
}
// Returns tags for this post.
public function getTags()
{
return $this->tags;
}
// Adds a new tag to this post.
36
37
38
39
40
41
42
43
44
45
46
379
<?php
//...
use Doctrine\Common\Collections\ArrayCollection;
class Tag
{
// ...
/**
* @ORM\ManyToMany(targetEntity="\Application\Entity\Post", mappedBy="tags")
*/
protected $posts;
// Constructor.
public function __construct()
{
$this->posts = new ArrayCollection();
}
// Returns posts associated with this tag.
public function getPosts()
{
23
24
25
26
27
28
29
30
31
380
return $this->posts;
}
// Adds a post into collection of posts related to this tag.
public function addPost($post)
{
$this->posts[] = $post;
}
}
In the code above, we by analogy define the other side of the relation and getter/setter methods
for retrieving the collection of posts associated with the tag, and adding posts associated with
the given tag.
<?php
namespace Application;
return array(
// ...
'doctrine' => array(
'driver' => array(
__NAMESPACE__ . '_driver' => array(
'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
'cache' => 'array',
'paths' => array(__DIR__ . '/../src/' . __NAMESPACE__ . '/Entity')
),
'orm_default' => array(
'drivers' => array(
__NAMESPACE__ . '\Entity' => __NAMESPACE__ . '_driver'
)
)
)
)
);
Above, in line 2, we specify the namespace Application. This should be the name of the current
module.
Note that usually we do not specify namespace in config files, but in this particular
case it is convenient to do. When we have namespace defined, we can use the __NAMESPACE__ placeholder which expands into that namespace.
381
In line 6, we have doctrine key, under which we have the driver subkey. In line 11, we tell
Doctrine ORM that our entities are stored inside of Application/Entity directory under the
modules src directory.
retrieve entities from their repositories using search criteria and save entities back to
repositories.
EntityManager is registered as a service in the Zend Framework 2 service manager (see Chapter
3 for additional information about service manager). In your controller class, you retrieve the
EntityManager from service manager as follows (if you need a different connection than orm_default, just replace the orm_default with the required connection name):
// Get Doctrine entity manager
$entityManager = $this->getServiceLocator()
->get('doctrine.entitymanager.orm_default');
The most used methods provided by the EntityManager class are listed in table 12.6 below.
Table 12.6. Methods of the EntityManager
Method
Description
persist($entity)
remove($entity)
flush()
createQuery($dql)
getRepository($entityName)
382
The createQuery() method is designed for creating a query from a DQL string. It returns the
Query object. You then execute the query and get results (an array of entities matching search
conditions).
The getRepository() method is designed to get repository by entity class name. Please look
below for example where we get the repository for our Post entity:
$repository = $entityManager->getRepository('\Application\Entity\Post')
The most used methods provided by the EntityRepository class are listed in table 12.6.
Table 12.6. Methods of the EntityRepository
Method
Description
findAll()
find($id)
findBy($criteria, $orderBy, $limit,
$offset)
383
Method
Description
findOneBy($criteria, $orderBy)
createQueryBuilder($alias)
The findAll() method gets all entities from repository. For simple example of its usage, look
below:
// Get Doctrine entity manager
$posts = $entityManager->getRepository('\Application\Entity\Post')->findAll();
The find() method is the simplest method of searching for an entity. It retrieves an entity by its
ID (primary key).
In the example below, we select post with ID = 1.
// Get Doctrine entity manager
$post = $entityManager->getRepository('\Application\Entity\Post')->find(1);
The findBy() takes a search criteria (and optional sorting order and limit) arguments and returns
a collection of entities matching criteria. The findOneBy() method is very similar to findBy(),
but it returns the first entity matching the criteria.
In the code example below, we use the findBy() method for selecting 50 most recent published
posts:
// Get Doctrine entity manager
$posts = $entityManager->getRepository('\Application\Entity\Post')->findBy(
array('status'=>Post::STATUS_PUBLISHED),
array('$dateCreated'=>'DESC'), 50);
And the most complex search method is the createQueryBuilder(). That method allows to
create complex DQL queries. For information on using query builder, please refer to Doctrine
documentation.
If standard find methods are not sufficient (or if you have complex search criterias and DQL
queries), you can create your own repository by extending the standard EntityRepository class
and encapsulate the search logic there. We will show how to do that later when implementing
tag cloud feature for our Blog sample.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
384
<?php
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class IndexController extends AbstractActionController {
// This is the default "index" action of the controller. It displays the
// Posts page containing the recent blog posts.
public function indexAction()
{
// Get Doctrine entity manager
$entityManager = $this->getServiceLocator()
->get('doctrine.entitymanager.orm_default');
// Get recent posts
$posts = $entityManager->getRepository('\Application\Entity\Post')
->findBy(array('status'=>Post::STATUS_PUBLISHED),
array('dateCreated'=>'DESC'));
// Render the view template
return new ViewModel(array(
'posts' => $posts
));
}
}
In the code above, we retrieve the EntityManager service from ZF2 service manager (line 14).
We get the repository of the Post entities with entity managers getRepository() method (line
18). With the findBy() method provided by repository, we select published posts sorted by date
in descending order (line 19). And, in line 23 we pass the selected posts to the view for rendering.
Next, modify the index.phtml view template file in application/index directory under modules
view directory and put the following content into it:
1
2
3
4
5
6
7
8
9
10
<h1>Posts</h1>
<?php foreach($posts as $post): ?>
<h3>
<a href="#">
<?php echo $post->getTitle(); ?>
</a>
</h3>
385
11
12
13
14
15
<p>
<?php echo $post->getContent(); ?>
</p>
<?php endforeach; ?>
In the view template above, we go in turn through the posts we selected and render each ones
title and content. That simple!
Now, if you open the Blog web application in your browser, you should be able to see the
following page containing the list of posts (look at figure 12.3 below).
386
the PostController::addAction() controller action method will be used for getting form
data, and calling PostManager for saving the data to database.
and add.phtml view template will render the form.
<?php
namespace Application\Form;
use Zend\Form\Form;
use Zend\InputFilter\InputFilter;
/**
* This form is used to collect post data.
*/
class PostForm extends Form
{
/**
* Constructor.
*/
public function __construct()
{
// Define form name
parent::__construct('post-form');
// Set POST method for this form
$this->setAttribute('method', 'post');
$this->addElements();
$this->addInputFilter();
}
/**
* This method adds elements to form (input fields and submit button).
*/
protected function addElements()
{
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
387
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
388
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
389
$inputFilter->add(array(
'name'
=> 'content',
'required' => true,
'filters' => array(
array('name' => 'StripTags'),
),
'validators' => array(
array(
'name'
=> 'StringLength',
'options' => array(
'min' => 1,
'max' => 4096
),
),
),
)
);
$inputFilter->add(array(
'name'
=> 'tags',
'required' => true,
'filters' => array(
array('name' => 'StringTrim'),
array('name' => 'StripTags'),
array('name' => 'StripNewLines'),
),
'validators' => array(
array(
'name'
=> 'StringLength',
'options' => array(
'min' => 1,
'max' => 1024
),
),
),
)
);
}
}
As you can see from the code above, the PostForm class defines a ZF2 form with title, content,
tags, and status fields. It also has the Submit button.
Since we covered forms in details in Chapter 7, here we do not explain the code
presented above deeply.
390
Create the PostManager.php file inside the Application/Service directory under the modules
source directory. Put the following content into that file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
namespace Application\Service;
use Zend\ServiceManager\ServiceManager;
use Zend\ServiceManager\ServiceManagerAwareInterface;
use Application\Entity\Post;
use Application\Entity\Comment;
use Application\Entity\Tag;
use Zend\Filter\StaticFilter;
// The PostManager service is responsible for adding new posts.
class PostManager implements ServiceManagerAwareInterface
{
// Service manager.
private $serviceManager = null;
// Sets service manager.
public function setServiceManager(ServiceManager $serviceManager)
{
$this->serviceManager = $serviceManager;
}
// Returns service manager.
public function getServiceLocator()
{
return $this->serviceManager;
}
// This method adds a new post.
public function addNewPost($title, $content, $tags, $status)
{
// Get Doctrine entity manager.
$entityManager = $this->getServiceLocator()
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
->get('doctrine.entitymanager.orm_default');
// Create new Post entity.
$post = new Post();
$post->setTitle($title);
$post->setContent($content);
$post->setStatus($status);
$currentDate = date('Y-m-d H:i:s');
$post->setDateCreated($currentDate);
// Add the entity to entity manager.
$entityManager->persist($post);
// Add tags to post
$this->addTagsToPost($tags, $post);
// Apply changes to database.
$entityManager->flush();
}
// Adds/updates tags in the given post.
private function addTagsToPost($tagsStr, $post)
{
// Get Doctrine entity manager.
$entityManager = $this->getServiceLocator()
->get('doctrine.entitymanager.orm_default');
// Remove tag associations (if any)
$tags = $post->getTags();
foreach ($tags as $tag) {
$post->removeTag($tag);
}
// Add tags to post
$tags = explode(',', $tagsStr);
foreach ($tags as $tagName) {
$tagName = StaticFilter::execute($tagName, 'StringTrim');
if (empty($tagName)) {
continue;
}
$tag = $entityManager->getRepository('\Application\Entity\Tag')
->findOneBy(array('name' => $tagName));
if ($tag == null)
$tag = new Tag();
391
79
80
81
82
83
84
85
86
87
392
$tag->setName($tagName);
$tag->addPost($post);
$entityManager->persist($tag);
$post->addTag($tag);
}
}
}
As you can see from the code above, our service model implements the ServiceManagerAwareInterface
interface (line 11). When we use service manager to instantiate this service, it will automatically
call the setServiceManager() method on the service and pass an instance of ServiceManager
to it as an argument (line 17). We save the service manager as a private property for further use.
In lines 29-51, we have the addNewPost() public method which takes post title, content, commaseparated list of tags, and status as arguments. It then creates a new instance of Post entity
(line 36) and fills its properties with user-provided data. It uses the EntityManagers persist()
method (line 44) to add the newly created entity to entity manager. The addTagsToPost() private
method is called (line 47) to assign the post with one or several tags. And the flush() method is
used for applying changes to database in a single transaction (line 50).
The addTagsToPost() private method contains logic for removing old associations between the
post and tags (lines 61-64), then parsing comma-separated list of tags (line 67), and assigning
new tags to the post (lines 75-84).
Finally, we register PostManager service by modifying module.config.php configuration file as
follows:
1
2
3
4
5
6
7
8
9
10
11
12
<?php
//...
return array(
//...
'service_manager' => array(
//...
'invokables' => array(
'post_manager'=>'Application\Service\PostManager',
),
),
//...
);
Now, you can retrieve a singleton instance of the PostManager service using controller-provided
getServiceLocator() method, as shown in example below:
$postManager = $this->getServiceLocator()->get('post_manager');
393
41
42
43
44
45
394
Above, in line 12, we create an instance of PostForm form. In line 14, we get singleton instance
of the PostManager service.
In line 17, we check whether this is a POST request. If the request is a POST request, we fill
the form with input data and validate the data. In case of valid data, we call the addNewPost()
method on the PostManager service (line 31), and redirect the user to the list of posts.
Finally, we add the view template. Create the add.phtml file in application/post directory under
modules view directory and put the following content into it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
$form = $this->form;
$form->get('title')->setAttributes(array(
'class'=>'form-control',
'placeholder'=>'Enter post title here'
));
$form->get('content')->setAttributes(array(
'class'=>'form-control',
'placeholder'=>'Type content here',
'rows'=>6
));
$form->get('tags')->setAttributes(array(
'class'=>'form-control',
'placeholder'=>'comma, separated, list, of, tags'
));
$form->get('status')->setAttributes(array(
'class'=>'form-control'
));
$form->get('submit')->setAttributes(array('class'=>'btn btn-primary'));
$form->prepare();
?>
<h1>Add New Post</h1>
<p>
Please fill out the following form and click the <i>Create</i> button.
</p>
<div class="row">
<div class="col-md-6">
395
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
</div>
<div class="form-group">
<?php echo $this->formLabel($form->get('content')); ?>
<?php echo $this->formElement($form->get('content')); ?>
<?php echo $this->formElementErrors($form->get('content')); ?>
</div>
<div class="form-group">
<?php echo $this->formLabel($form->get('tags')); ?>
<?php echo $this->formElement($form->get('tags')); ?>
<?php echo $this->formElementErrors($form->get('tags')); ?>
</div>
<?php echo $this->formElement($form->get('submit')); ?>
<?php echo $this->form()->closeTag(); ?>
</div>
</div>
Now, if you open the URL http://localhost/application/post/add in your web browser, you should
see the Add New Post page like shown in figure 12.5 below:
396
Filling the form and clicking the Create button results in saving the new post to database. Then
you are able to see the newly created post in the list of posts at the Home page.
397
to create a form that would allow to enter post title, content, etc. For this page, we can
successfully reuse the PostForm form we created earlier (we just rename the Create button
caption into Save).
to add updatePost() method to the PostManager service. The method would find the post
by ID in database and update its data;
to add convertTagsToString() method to the PostManager service. This method would
take the post entity, and on output produce string containing comma-separated list of tags;
to add the PostController::editAction() action method that would take user input, pass
it to models and return data for rendering;
and add the edit.phtml view template file that would render the form.
<?php
//...
class PostManager implements ServiceManagerAwareInterface
{
//...
// This method allows to update data of a single post.
public function updatePost($post, $title, $content, $tags, $status)
{
// Get Doctrine entity manager.
$entityManager = $this->getServiceLocator()
->get('doctrine.entitymanager.orm_default');
$post->setTitle($title);
$post->setContent($content);
$post->setStatus($status);
// Add tags to post
$this->addTagsToPost($tags, $post);
// Apply changes to database.
$entityManager->flush();
}
// Converts tags of the given post to comma separated list (string).
public function convertTagsToString($post)
{
$tags = $post->getTags();
$tagCount = count($tags);
30
31
32
33
34
35
36
37
38
39
40
41
398
$tagsStr = '';
$i = 0;
foreach ($tags as $tag) {
$i ++;
$tagsStr .= $tag->getName();
if ($i < $tagCount)
$tagsStr .= ', ';
}
return $tagsStr;
}
}
Above, we have the updatePost() method (lines 8-24) that takes an existing Post entity, the new
title, content, status and the list of tags. It then updates entitys properties and saves changes to
database using flush() method.
Note that the updatePost() method doesnt use the persist() method of entity
manager, because here we have existing post, not a new one.
Then, we have the convertTagsToString() method (lines 26-39) which takes the post, goes
through Tag entities associated with the post and formats and returns the comma-separated
list of tags.
<?php
namespace Application\Controller;
//...
use Application\Form\PostForm;
use Application\Entity\Post;
class PostController extends AbstractActionController
{
// This action displays the page allowing to edit a post.
public function editAction()
{
// Create the form.
$form = new PostForm();
// Get post ID.
$postId = $this->params()->fromRoute('id', -1);
399
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
400
$form->setData($data);
}
// Render the view template.
return new ViewModel(array(
'form' => $form,
'post' => $post
));
}
}
In the code above, we extract the post ID using the fromRoute() method of the params()
controller plugin. Then we search for post having such ID using the findOneBy() method
provided by the entity repository.
Then we check if this is a POST request. If this is the POST request, we fill in and validate the
form with POST data. Then we use the updatePost() method of the PostManager service.
Finally, create the application/post/edit.phtml file under the modules view directory. Place the
following content there:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
$form = $this->form;
$form->get('title')->setAttributes(array(
'class'=>'form-control',
'placeholder'=>'Enter post title here'
));
$form->get('content')->setAttributes(array(
'class'=>'form-control',
'placeholder'=>'Type content here',
'rows'=>6
));
$form->get('tags')->setAttributes(array(
'class'=>'form-control',
'placeholder'=>'comma, separated, list, of, tags'
));
$form->get('status')->setAttributes(array(
'class'=>'form-control'
));
$form->get('submit')->setAttributes(array('class'=>'btn btn-primary'));
$form->get('submit')->setValue('Save');
$form->prepare();
?>
<h1>Edit Post</h1>
401
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<p>
Please fill out the following form and click the *Save* button.
</p>
<div class="row">
<div class="col-md-6">
<?php echo $this->form()->openTag($form); ?>
<div class="form-group">
<?php echo $this->formLabel($form->get('title')); ?>
<?php echo $this->formElement($form->get('title')); ?>
<?php echo $this->formElementErrors($form->get('title')); ?>
</div>
<div class="form-group">
<?php echo $this->formLabel($form->get('content')); ?>
<?php echo $this->formElement($form->get('content')); ?>
<?php echo $this->formElementErrors($form->get('content')); ?>
</div>
<div class="form-group">
<?php echo $this->formLabel($form->get('tags')); ?>
<?php echo $this->formElement($form->get('tags')); ?>
<?php echo $this->formElementErrors($form->get('tags')); ?>
</div>
<?php echo $this->formElement($form->get('submit')); ?>
<?php echo $this->form()->closeTag(); ?>
</div>
</div>
Now, if you open the URL http://localhost/application/post/edit/<id> in your web browser, you
should be able to see the Edit Post page that allows to edit an existing post (see the figure 12.5
402
below):
403
associations. Site visitor will be able to trigger the action by entering the following URL in
browsers navigation bar: http://localhost/application/post/delete/<id>, where <id> is the unique
identifier of the post. Finally, the action redirects the site visitor to the Admin page.
<?php
//...
class PostManager implements ServiceManagerAwareInterface
{
//...
// Removes post and all associated comments.
public function removePost($post)
{
$entityManager = $this->getServiceLocator()
->get('doctrine.entitymanager.orm_default');
// Remove associated comments
$comments = $post->getComments();
foreach ($comments as $comment) {
$entityManager->remove($comment);
}
// Remove tag associations (if any)
$tags = $post->getTags();
foreach ($tags as $tag) {
$post->removeTagAssociation($tag);
}
$entityManager->remove($post);
$entityManager->flush();
}
}
In the code above, we first retrieve all comments associated with the post using the getComments()
method of the Post entity. Then we call EntityManagers remove() method and pass it each
comment that we want to remove.
Next, we get all tags associated with the post by calling Posts getTags() method. We remove
association between the post and tag (but not tag itself!) with the help of the Posts removeTag()
method (see below for the code of the method).
404
Finally, we remove the post itself by calling the EntityManagers remove() method. We apply
changes to database with the flush() method.
And here is the code of the Post::removeTagAssociation() method:
1
2
3
4
5
<?php
//..
class PostController extends AbstractActionController
{
// This "delete" action displays the Delete Post page.
public function deleteAction()
{
$postId = $this->params()->fromRoute('id', -1);
$entityManager = $this->getServiceLocator()
->get('doctrine.entitymanager.orm_default');
$post = $entityManager->getRepository('\Application\Entity\Post')
->findOneBy(array('id'=>$postId));
if ($post == null) {
$this->getResponse()->setStatusCode(404);
return;
}
$postManager = $this->getServiceLocator()->get('post_manager');
$postManager->removePost($post);
// Redirect the user to "index" page.
return $this->redirect()->toRoute('application/default',
array('controller'=>'post', 'action'=>'admin'));
}
}
405
to create the form that would allow to enter the comment and its authors name;
to modify the PostManager and add all necessary business logic;
to create PostController::viewAction() controllers action;
and to create the view.phtml view template.
406
407
<?php
namespace Application\Form;
use Zend\Form\Form;
use Zend\InputFilter\InputFilter;
/**
* This form is used to collect comment data.
*/
class CommentForm extends Form
{
// Constructor.
public function __construct()
{
// Define form name
parent::__construct('comment-form');
// Set POST method for this form
$this->setAttribute('method', 'post');
$this->addElements();
$this->addInputFilter();
}
// This method adds elements to form (input fields and submit button).
protected function addElements()
{
// Add "author" field
$this->add(array(
'type' => 'text',
'name' => 'author',
'attributes' => array(
'id' => 'author'
),
'options' => array(
'label' => 'Author',
),
));
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
408
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
409
$inputFilter->add(array(
'name'
=> 'comment',
'required' => true,
'filters' => array(
array('name' => 'StripTags'),
),
'validators' => array(
array(
'name'
=> 'StringLength',
'options' => array(
'min' => 1,
'max' => 4096
),
),
),
)
);
}
}
As you see from the code above, the CommentForm form contains the author, comment fields, and
the Submit button.
Since we covered forms in details in Chapter 7, here we do not explain the code
presented above deeply.
<?php
//...
/**
* The PostManager service is responsible for adding new posts.
*/
class PostManager implements ServiceManagerAwareInterface
{
//...
// Returns count of comments for given post as properly formatted string.
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
410
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
namespace Application\Controller;
use
use
use
use
use
use
Zend\Mvc\Controller\AbstractActionController;
Zend\View\Model\ViewModel;
Application\Form\PostForm;
Application\Entity\Post;
Application\Form\CommentForm;
Application\Entity\Comment;
/**
* This is the Post controller class of the Blog application.
* This controller is used for managing posts (adding/editing/viewing/deletin\
g).
*/
class PostController extends AbstractActionController
{
/**
* This action displays the "View Post" page allowing to see the post title
* and content. The page also contains a form allowing
* to add a comment to post.
*/
public function viewAction()
{
$postId = $this->params()->fromRoute('id', -1);
$entityManager = $this->getServiceLocator()->get('doctrine.entitymanager.\
orm_default');
$postManager = $this->getServiceLocator()->get('post_manager');
$post = $entityManager->getRepository('\Application\Entity\Post')
->findOneBy(array('id'=>$postId));
if ($post == null) {
$this->getResponse()->setStatusCode(404);
return;
}
$commentCount = $postManager->getCommentCountStr($post);
// Create the form.
$form = new CommentForm();
// Check whether this post is a POST request.
if($this->getRequest()->isPost()) {
411
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
Finally, add the view.phtml view template file and put the following content there:
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$form = $this->form;
$form->get('author')->setAttributes(array(
'class'=>'form-control',
'placeholder'=>'Author\'s name'
));
$form->get('comment')->setAttributes(array(
'class'=>'form-control',
'rows'=>6,
'placeholder'=>'Text'
));
$form->get('submit')->setAttributes(array('class'=>'btn btn-primary'));
$form->prepare();
412
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
?>
<a href="
<?php echo $this->url('application/default',
array('controller'=>'index', 'action'=>'index')); ?>">
<< Back to list of posts
</a>
<h1>
<?php echo $post->getTitle(); ?>
</h1>
<p class="comments-header">
<?php echo $postManager->getCommentCountStr($post); ?> |
<a href="#comment">
Add new comment
</a>
</p>
<p>
<?php echo $post->getContent(); ?>
</p>
<p>
Tags: <?php echo $postManager->convertTagsToString($post); ?>
</p>
<h3><?php echo $postManager->getCommentCountStr($post); ?></h3>
<?php foreach ($post->getComments() as $comment): ?>
<hr>
<p>
<?php echo $comment->getAuthor() ?> on
<?php echo $comment->getDateCreated(); ?>
</p>
<p>
<?php echo $comment->getContent(); ?>
</p>
<?php endforeach; ?>
<hr>
413
414
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<a name="comment"></a>
<h3>Leave Reply</h3>
<div class="row">
<div class="col-md-8">
<?php echo $this->form()->openTag($form); ?>
<div class="form-group">
<?php echo $this->formLabel($form->get('author')); ?>
<?php echo $this->formElement($form->get('author')); ?>
<?php echo $this->formElementErrors($form->get('author')); ?>
</div>
<div class="form-group">
<?php echo $this->formLabel($form->get('comment')); ?>
<?php echo $this->formElement($form->get('comment')); ?>
<?php echo $this->formElementErrors($form->get('comment')); ?>
</div>
<?php echo $this->formElement($form->get('submit')); ?>
<?php echo $this->form()->closeTag(); ?>
</div>
</div>
<?php
//..
class PostController extends AbstractActionController
{
/**
* This "admin" action displays the Manage Posts page. This page contains
* the list of posts with an ability to edit/delete any post.
*/
public function adminAction()
{
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
415
Finally, add the corresponding view template file admin.phtml to the application/post directory
under modules view directory:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<h1>Manage Posts</h1>
<p>
<a class="btn btn-default" href="
<?php echo $this->url('application/default',
array('controller'=>'post', 'action'=>'add')); ?>">
New Post
</a>
</p>
<table class="table table-striped">
<tr>
<th>ID</th>
<th>Post Title</th>
<th>Date Created</th>
<th>Status</th>
<th>Actions</th>
</tr>
<?php foreach ($posts as $post): ?>
<tr>
<td><?php echo $post->getId(); ?></td>
<td>
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
416
417
418
to modify the PostManager and add functionality for calculating font sizes for the tag
cloud;
to add controllers action and corresponding view template.
DQL is similar to SQL in sense that it allows to write and execute queries to database,
but result of a query is an array of objects rather than an array of table rows. For more
information on DQL and its usage examples, please refer to this page.
For our Blog sample web application, we need a custom repository which allows to find
published posts having at least one tag (to calculate total count of tagged posts), and, to find
published posts filtered by particular tag. We plan to encapsulate this search logic into the custom
PostRepository repository.
Doctrine works with custom repositories transparently. This means, that you retrieve
the repository from EntityManager as usual and still can use its findBy(), findOneBy()
and other methods.
Below, you can find the code of PostRepository class that has two public methods:
the findPostsHavingAnyTag() method is designed to select all posts that have status
Published and have one or more tags assigned;
and the findPostsByTag() method is designed to return all published posts that have the
particular tag assigned (to filter posts by the given tag).
http://docs.doctrine-project.org/en/latest/reference/dql-doctrine-query-language.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
419
<?php
namespace Application\Repository;
use Doctrine\ORM\EntityRepository;
use Application\Entity\Post;
// This is the custom repository class for Post entity.
class PostRepository extends EntityRepository
{
// Finds all published posts having any tag.
public function findPostsHavingAnyTag()
{
$entityManager = $this->getEntityManager();
$dql = "SELECT p FROM \Application\Entity\Post p JOIN p.tags t WHERE p.st\
atus=".Post::STATUS_PUBLISHED." ORDER BY p.dateCreated DESC";
$query = $entityManager->createQuery($dql);
$posts = $query->getResult();
return $posts;
}
// Finds all published posts having the given tag.
public function findPostsByTag($tagName)
{
$entityManager = $this->getEntityManager();
$dql = "SELECT p FROM \Application\Entity\Post p JOIN p.tags t WHERE p.st\
atus=".Post::STATUS_PUBLISHED." AND t.name='".$tagName."' ORDER BY p.dateCrea\
ted DESC";
$query = $entityManager->createQuery($dql);
$posts = $query->getResult();
return $posts;
}
}
To let Doctrine know that it should use the custom repository for Post entity, modify the Post
entitys annotation as follows:
1
2
3
4
5
6
7
8
9
10
11
12
420
<?php
//...
/**
* This class represents a single post in a blog.
* @ORM\Entity(repositoryClass="\Application\Repository\PostRepository")
* @ORM\Table(name="post")
*/
class Post
{
//...
}
Above, in line 6, we use the repositoryClass parameter of the @ORM\Entity tag to tell Doctrine
that it should use PostRepository repository.
<?php
//...
class PostManager implements ServiceManagerAwareInterface
{
//...
// Calculates frequencies of tag usage.
public function getTagCloud()
{
$tagCloud = array();
$entityManager = $this->getServiceLocator()
->get('doctrine.entitymanager.orm_default');
$posts = $entityManager->getRepository('\Application\Entity\Post')
->findPostsHavingAnyTag();
$totalPostCount = count($posts);
$tags = $entityManager->getRepository('\Application\Entity\Tag')
->findAll();
foreach ($tags as $tag) {
$postsByTag = $entityManager->getRepository('\Application\Entity\Post')
->findPostsByTag($tag->getName());
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
421
$postCount = count($postsByTag);
if ($postCount > 0) {
$tagCloud[$tag->getName()] = $postCount;
}
}
$normalizedTagCloud = array();
// Normalize
foreach ($tagCloud as $name=>$postCount) {
$normalizedTagCloud[$name] = $postCount/$totalPostCount;
}
return $normalizedTagCloud;
}
}
<?php
//...
class IndexController extends AbstractActionController
{
public function indexAction()
{
$tagFilter = $this->params()->fromQuery('tag', null);
// Get Doctrine entity manager
$entityManager = $this->getServiceLocator()
->get('doctrine.entitymanager.orm_default');
if ($tagFilter) {
// Filter posts by tag
$posts = $entityManager->getRepository('\Application\Entity\Post')
->findPostsByTag($tagFilter);
} else {
// Get recent posts
$posts = $entityManager->getRepository('\Application\Entity\Post')
->findBy(array('status'=>Post::STATUS_PUBLISHED),
array('dateCreated'=>'DESC'));
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
422
}
// Get post manager service.
$postManager = $this->getServiceLocator()->get('post_manager');
// Get popular tags.
$tagCloud = $postManager->getTagCloud();
// Render the view template.
return new ViewModel(array(
'posts' => $posts,
'postManager' => $postManager,
'tagCloud' => $tagCloud
));
}
}
The action method will retrieve the tag from the GET variable tag if the variable doesnt present
in HTTP request, all posts are retrieved as usual. If the variable present, we use our custom
repositorys findPostsByTag() method to filter posts.
In line 15, we call the PostManager::getTagCloud() that returns array of tags and their
frequencies. We use this information for rendering the cloud.
<?php
$this->headTitle('Posts');
$this->mainMenu()->setActiveItemId('home');
$this->pageBreadcrumbs()->setItems(array(
'Home'=>$this->url('home')
));
?>
<h1>Posts</h1>
<div class="row">
<div class="col-md-8">
<?php foreach($posts as $post): ?>
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<h3>
<a href="<?php echo $this->url('application/default',
array('controller'=>'post', 'action'=>'view',
'id'=>$post->getId())); ?>">
<?php echo $post->getTitle(); ?>
</a>
</h3>
<p>
Tags: <?php echo $postManager->convertTagsToString($post); ?>
</p>
<p class="comments-header">
<?php echo $postManager->getCommentCountStr($post); ?> |
<a href="<?php echo $this->url('application/default',
array('controller'=>'post', 'action'=>'view',
'id'=>$post->getId()), array('fragment'=>'comment')); ?>">
Add new comment
</a>
</p>
<p>
<?php echo $post->getContent(); ?>
</p>
<?php endforeach; ?>
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Popular Tags</h3>
</div>
<div class="panel-body">
<?php foreach($this->tagCloud as $tagName=>$frequency): ?>
<a href="<?php echo $this->url('application/default',
array('controller'=>'index', 'action'=>'index'),
array('query'=>array('tag'=>$tagName))); ?>">
<span style="font-size:<?php echo $frequency*3 ?>em">
<?php echo $tagName; ?>
</span>
</a>
423
65
66
67
68
69
424
This chapter is currently being developed and is not yet complete. You will get all
updates for free as they appear. If you see some mistake, please contact the author
using the following E-mail address: [email protected]. The author will be more
than happy to answer you and fix the mistake.
12.15 Summary
Doctrine is not part of Zend Framework 2, and we cover its usage in this book, because it provides
an easy way of accessing a database.
In this chapter, weve considered the usage of the Object Relational Mapper (ORM) component
of Doctrine library. The ORM is designed for database management in object-oriented style.
With ORM, you map a database table to a PHP class called entity, and columns of that table are
mapped to the properties of the class.
To load data from the database, you retrieve an entity from its repository. The repository is a class
that can be considered as a collection of all available entities. When you request the repository
for an entity, it connects to the database, loads the data from the table mapped to the entity, and
assigns entitys fields with the data.
Configuring the development environment is the first thing you have to do when beginning with
creating your first web-site. This includes installing a web server, the PHP engine with required
extensions and a database.
For the purpose of running the code examples created in this book, we will use Apache HTTP
Server (v.2.2 or later), PHP engine (v.5.3 or later) with XDebug extension and MySQL database
(v.5.5 or later).
We also provide instructions for installing NetBeans IDE, which is a convenient integrated
development environment for PHP development. It allows for an easier navigation, editing and
debugging of your PHP application. The NetBeans IDE is written in Java and can be installed in
Windows, Linux and other platforms supporting a compatible Java machine.
426
software into their repository, so if you want to install one, you will need to download it from
somewhere, read the manual, and possibly (if you are not lucky) compile it yourself.
There is another Linux distribution, which, in the authors opinion, suits better for PHP
development. Its name is Linux Ubuntu. Ubuntu is being developed by Canonical Ltd. Linux
Ubuntu has two editions: Desktop edition and Server edition. Ubuntu Desktop is a distribution
containing graphics environment, while Ubuntu Server edition has console terminal only. For
the purpose of PHP development, you will need Desktop edition.
Canonical typically releases a new version of Linux Ubuntu each 6 months, in April and October,
and a long term support (LTS) version each 2 years. For example, at the moment of writing
this text, the latest version is Ubuntu 14.04 Trusty Tahr LTS (released in April 2014).
Non-LTS releases have short support period (about 9 months), but they have the newest versions
of the PHP software out of the box. On the other hand, LTS releases have longer support period
(5 years), but a little outdated PHP software out of the box. For PHP development, the author
would recommend to use the latest non-LTS version of Ubuntu Desktop, because it has the newest
version of PHP and other software available from repository. The disadvantage of using such a
version is that you will need to upgrade it to the next release every 9 months (as support period
expires).
For your information, table A.1 lists PHP versions avaiable for installation from repository in
different Linux distributions:
Table A.1. Available PHP versions in different Linux distributions
Linux Distribution
PHP Version
5.5.4
5.4.9
5.4.6
5.3.10
5.4.4
5.3.3
5.3.3
5.4.15
5.5.0
5.4.9
When choosing between 32-bit and 64-bit versions of the system, remember that the 64 bit
version of Linux Ubuntu will have more compatibility issues than its 32-bit counterpart. The
support of drivers can also cause problems on the 64-bit platform.
427
install packages in Debian-based Linux distributions (e.g. Ubuntu Linux), you use Advanced
Packaging Tool (APT). In Red Hat provided distributions (e.g. Fedora or CentOS), you use YUM
(RPM package manager). Below, detailed installation instructions for these operating systems are
provided.
Debian or Linux Ubuntu
First of all, it is recommended that you update your system by installing the latest available
updates. To do this, from a command shell, run the following commands:
sudo apt-get update
sudo apt-get upgrade
The commands above runs the APT tool and install the newest system packages updates. The
sudo command (stands for Super User DO) allows to run another command, apt-get in our
case, as system administrator (root). You typically use sudo when you need to elevate your
privileges to install a package or edit some configuration file.
The sudo command may request you for password. When prompted, enter the password under which you log into the system and press Enter.
The commands above download from repository and install the latest available versions of
Apache HTTP Server, PHP engine and PHP extension module for Apache.
The commands above may ask you for confirmation when installing a package. It is
recommended to answer Yes (press y and then press Enter).
The command above runs the YUM tool and installs the newest system package updates.
Next, from a command shell, run the following commands:
sudo yum install httpd
sudo yum install php
The commands above download from repository and install the latest available versions of
Apache HTTP Server and PHP engine.
Next, run the following commands to add Apache HTTP Server to system autorun and start it:
428
The following command installs Midnight Commander in Fedora, CentOS or Red Hat
Linux:
sudo yum install mc
After installation, you can edit a text file with the command like this:
mcedit /path/to/file
If you need administrative permissions to edit the file, prepend the sudo command to
the command above.
Open the file in your web browser. The standard PHP information page should display (see figure
A.1 for example).
Type the following to edit php.ini in Fedora, CentOS or Red Hat Linux:
sudo mcedit /etc/php.ini
429
For the development environment, it is recommended to set the following error handling and
logging parameters as below. This will force PHP to display errors on your PHP pages to screen.
error_reporting = E_ALL
display_errors = On
display_startup_errors = On
Set your time zone settings (replace <your_time_zone> placeholder with your time zone, for
example, UTC or America/New_York):
date.timezone = <your_time_zone>
430
post_max_size=128M
upload_max_filesize=128M
When ready, save your changes by pressing the F2 key and then press F10 to exit from Midnight
Commanders editor.
[OK]
The command above creates a symbolic link in the mods-enabled directory, this way you enable
modules in modern versions of Apache web server.
Finally, restart Apache web server to apply your changes.
A symbolic link in Linux is an analog of a shortcut in Windows.
431
All you have to do is just to edit this virtual host file when needed and restart Apache to apply
changes.
You can also copy this file and create another virtual host, when you need several web sites to
operate on the same machine. For example, to create another virtual host file named vhost2 ,
type the following from your command shell:
cd /etc/apache2/sites-available
432
The virtual hosts name starts with a prefix (like 000, 010, etc.), which defines the
priority. Apache web server tries to direct an HTTP request to each virtual host in
turn (first to 000-default, then to 010-vhost2), and if a certain virtual host cannot serve
the request, the next one is tried and so on.
Finally, restart the Apache server to apply your changes. Then open the phpinfo.php in your
browser and look for XDebug section (it should look like in the figure A.2):
In Fedora, CentOS or Red Hat Linux
In these Linux distributions, installing XDebug is a little more difficult. Install XDebug package
with the following command:
yum install php-pecl-xdebug
433
[xdebug]
xdebug.remote_enable=1
xdebug.remote_handler=dbgp
xdebug.remote_mode=req
xdebug.remote_host=127.0.0.1
xdebug.remote_port=9000
xdebug.profiler_enable = 1
Restart the Apache web server to apply your changes. Then check the phpinfo.php in your
browser. If installation was successfull, youll see some XDebug-related information.
434
The commands above install MySQL server component, MySQL client component and MySQL
extension module for PHP, respectively.
Fedora, CentOS or Red Hat Linux
In order to install MySQL database, type the following:
sudo yum install mysql-server
sudo yum install mysql
sudo yum install php-mysql
The commands above install MySQL server component, MySQL client component and MySQL
extension module for PHP, respectively.
Run the following commands to add MySQL server to autostart and start the server:
sudo chkconfig --level 235 mysqld on
sudo service mysqld start
http://www.mysql.com/
435
The MySQL command prompt will appear. In the command prompt enter the following
command and press Enter (in the command below, replace the <your_password> placeholder
with some password):
SET PASSWORD FOR 'root'@'localhost' = PASSWORD('<your_password>');
Now we need to create a new database schema that will store the tables. To do this, type the
following:
CREATE SCHEMA test_db;
The command above creates empty schema that we will populate later.
Next, we want to create another database user named test_user that will be used by the ZF2based web site for connecting to the database. To create the user, type the following (in the
command below, replace the <your_password> placeholder with some password):
GRANT ALL PRIVILEGES ON test_db.* TO 'test_user'@'localhost' IDENTIFIED BY '<your_password>';
The command above creates the user named test_user and grants the user all privileges on the
test_db database schema.
Finally, type quit to exit the MySQL prompt.
436
We need to download the binary installer for Windows, so on the page that appears, click binaries
link (see the figure A.4).
On the next page click win32 link. In this page, look for httpd-2.2.22-win32-x86-openssl-0.9.8t.msi
download link and click it to download the MSI installer (see the figure A.5).
437
Run the installer and follow the instructions of the wizard. If everything is OK, you should see
the Apache icon in the system tray (see the figure A.6).
Next, download the current stable version (version 5.5 recommended) of PHP from PHP web
site. Click the Windows 5.5.0 binaries and source link and download VC11 x86 Thread Safe ZIP
archive. Then unpack the archive to C:\Program Files (x86)\PHP.
In order to enable PHP engine, you have to edit the web servers configuration file (httpd.conf ).
Typically, the Apache config file is located in C:Program Files (x86)Apache Software FoundationApache2.2\conf folder. You need to add the following lines to your Apache httpd.conf
configuration file to load the PHP-Module for Apache 2.2:
# Load PHP module
LoadModule php5_module "C:/Program Files (x86)/php/php5apache2_2.dll"
AddHandler application/x-httpd-php .php
# Specify path to php.ini
PHPIniDir "C:/Program Files (x86)/php"
After editing the config file, restart Apache. You can do this by clicking the Apache tray icon
and choosing Restart from the context menu.
In Windows Vista/Windows 7, httpd.conf file permissions may prevent you from
saving the changes to file. To be able to save your changes to httpd.conf, you should
run your editor as administrator. For example, if you use notepad editor, you should
run it as administrator by right-clicking its icon and choosing Run as administrator
from context menu.
http://www.php.net/downloads.php
438
439
<?php
phpinfo();
?>
Open the file in your browser. The standard PHP information page should display (figure A.8).
and remove the hash (#) sign from the beginning to uncomment the line. It should now look like
this:
440
The line you uncommented forces Apache to include the additional config file named httpdvhosts.conf. Open C:Program FilesApache Software FoundationApache2.2\conf\extra\httpd-vhosts.conf
in a text editor. This file contains an example virtual host configuration (see below).
#
# Use name-based virtual hosting.
#
NameVirtualHost *:80
#
# VirtualHost example:
# Almost any Apache directive may go into a VirtualHost
# container. The first VirtualHost section is used for
# all requests that do not match a ServerName or ServerAlias
# in any <VirtualHost> block.
#
<VirtualHost *:80>
ServerAdmin [email protected]
DocumentRoot "C:/Program Files (x86)/Apache Software Foundation\
/Apache2.2/htdocs"
ServerName dummy-host.localhost
ServerAlias www.dummy-host.localhost
ErrorLog "logs/dummy-host.localhost-error.log"
CustomLog "logs/dummy-host.localhost-access.log" common
</VirtualHost>
441
You can edit it as needed and restart Apache to apply the changes.
Right now, you dont need to edit this file, well do that in Chapter 2 when installing
the Hello World application. Now you just need to understand how to create virtual
hosts.
Finally, restart the Apache server to apply your changes. Then open the phpinfo.php in your
browser and look for XDebug section (it should look like in the figure A.9):
http://www.xdebug.org/download.php
442
443
If you prefer to administer MySQL server using a graphical tool, you can download
and install MySQL Workbench (GUI Tool) from MySQL download page.
Now we need to create a new database schema that will store the tables. To do this, type the
following in the MySQL client window:
CREATE SCHEMA test_db;
The command above creates an empty database schema that we will populate later. If the
command is executed successfully, the following message is displayed:
Query OK, 1 rows affected (0.05 sec)
Next, we want to create another database user named test_user that will be used by the web
site for connecting to the database. To create the user, type the following (in the command below,
replace the <your_password> placeholder with some password):
GRANT ALL PRIVILEGES ON test_db.* TO 'test_user'@'localhost' IDENTIFIED BY '<your_password>';
The command above creates the user named test_user and grants the user all privileges on the
test_db database schema.
If you found the installation procedure described above to be boring, you might want
to use an integrated solution (stack) for installing Apache HTTP Server, PHP engine
and MySQL database server using a single click. For an example of such an integrated
solution, visit the WAMP Server page.
http://www.wampserver.com/en/
444
The command above downloads from repository and installs NetBeans and all its dependent
packages. After the installation is complete, you can run netbeans by typing:
netbeans
445
To be able to create PHP projects, you need to activate PHP plugin for NetBeans. To do that,
open menu Tools->Plugins, the Plugins dialog appears. In the appeared dialog, click Settings
tab and set check marks to all Configuration and Update Centers (see the figure A.12).
Then click the Available Plugins tab. On that tab, click the Reload Catalog button to build the
list of all available plugins. Then in the list, set check mark to PHP plugin and click the Install
button (see the figure A.13).
When the PHP plugin installation is complete, restart the IDE. Then you should be able to create
new PHP projects from menu New->New Project....
It is also recommended to update NetBeans IDE to the latest version by opening menu
Help->Check for updates.
To install Java 7 (the latest stable version at the moment), run following command:
sudo apt-get install oracle-java7-installer
To ensure that Java 7 has been installed successfully, run the following command from your
command shell:
java -version
Next, download NetBeans installer from the NetBeans site (figure A.14). You may encounter
several NetBeans bundles available for download. Download the distribution that is intended for
PHP development.
https://netbeans.org/
446
447
When download has been finished, go to your Downloads directory and run the installer by
typing the commands below:
cd /Downloads
sudo chmod +x netbeans-7.3.1-php-linux.sh
./netbeans-7.3.1-php-linux.sh
Please note, that the installers file name may be different, depending on your version
of NetBeans.
When the installation is completed, you can run NetBeans by typing netbeans in the command
line. You should be able to see NetBeans splash screen, and then the IDE main window should
appear.
448
If your machine does not contain Java Runtime Environment (JRE), you should
download and install it before NetBeans installation. Java JRE can be downloaded from
Oracle site.
Summary
In this appendix, weve provided instructions on how to install and configure Apache HTTP
Server, PHP engine and MySQL database in both Linux and Windows platforms.
Weve also provided instructions for installing NetBeans integrated development environment
(IDE), which is a convenient integrated development environment for PHP development. It
allows you to navigate, edit and debug your ZF2-based application in an effective manner.
https://netbeans.org/
http://www.oracle.com/technetwork/java/javase/downloads/index.html
https://groups.google.com/forum/#!forum/using-zend-framework-2-book
449
Run Configuration
To be able to run and debug a web site, you first need to edit the sites properties. To do that,
in NetBeans Projects pane, right-click on the projects name, and from context menu select
Properties item. The projects Properties dialog will appear (shown in figure B.1).
451
In the left pane of the dialog that appears, click the Sources node. In the right pane, edit the Web
Root field to point to your web sites APP_DIR/public directory. You can do this by clicking the
Browse button to the right of the field. Then, in the dialog, click on the public directory and then
click Select Folder button (shown in figure B.2).
Next, click the Run Configuration node in the left pane. The right pane should display the run
settings for your site (figure B.3).
452
In the right pane, you can see that the current configuration is default. As an option, you can
create several run configurations.
Edit the fields as follows:
In the Run As field, select Local Website (running on local web server).
In the Project URL field, enter http://localhost. If you configured your virtual host to listen
on different port (say, on port 8080), enter the port number like this http://localhost:8080.
Keep the Index File field empty, because Apaches mod_rewrite module will mask our
actual index.php file.
In the Arguments field, you can specify which GET parameters to pass to your site through
the URL string. Typically, you keep this field empty.
Finally, click the OK button to close the Properties dialog.
453
If everything is OK with your run configuration, the default web browser will be launched, and
in the browser window, you will be able to see the Home page of the web site.
The same effect would have typing http://localhost/ in your browser, but NetBeans run toolbar
allows you to do that in a single click.
The lines above would print the value of the $var variable to the browsers output and then
stop program execution. While this can be used for debugging even a complex site, it makes the
debugging a pain, because you have to type variable dump commands in your PHP source file,
then refresh your web page in the browser to see the output, then edit the source file again, until
you determine the reason of the problem.
In contrast, when you debug your web site in NetBeans IDE, the PHP interpreter pauses program
execution flow at every line where you set a breakpoint. This makes it possible to conveniently
retrieve information about the current state of the program, like the values of the local variables
and the call stack. You see the debugging information in NetBeans window in graphical form.
To be able to debug the site, you need to have the XDebug extension installed. If you
havent installed it yet, please refer to Appendix A for additional information on how
to install it.
To start the debugging session, in NetBeans window, click the Debug button on the Run Toolbar
(figure B.4). Alternatively, you can press the CTRL+F5 key combination on your keyboard.
If everything is OK, you should be able to see the current program counter on the first code line
of the index.php file (shown in figure B.5):
454
While the program is in paused state, your web browsers window is frozen, because the browser
is waiting for data from the web server. Once you continue program execution, the browser
receives the data, and displays the web page.
Debug Toolbar
You can resume/suspend program execution with the Debug Toolbar (see figure B.6):
The Finish Debugger Session of the toolbar allows to stop the debugger. Press this button when
youre done with debugging the program. The same effect would have pressing the SHIFT+F5
key combination.
Clicking the Continue button (or pressing F5 key) continues the program execution until the next
breakpoint, or until the end of the program, if there are no more breakpoints.
455
The Step Over toolbar button (or pressing F8 key) moves the current program counter to the next
line of the program.
The Step Into toolbar button (or pressing F7 key) moves the current program counter to the next
line of the program, and if it is the function entry point, enters the function body. Use this when
you want to investigate your code in-depth.
The Step Out toolbar button (CTRL+F7) allows to continue program execution until returning
from the current function.
And Run to Cursor (F4) allows to continue program execution until the line of code where you
place the cursor. This may be convenient if you want to skip some code block and pause at a
certain line of your program.
Breakpoints
Typically, you set one or several breakpoints to the lines which you want to debug in step-bystep mode. To set a breakpoint, put your mouse to the left of the line of code where you want the
breakpoint to appear and click on the line number. Alternatively, you can put the cursor caret to
the line where you want to set a breakpoint and press CTRL+F8 key combination.
When you set the breakpoint, the line is marked with red color and a small red rectangle appears
to the left of it (shown in figure B.7):
456
You can travel between breakpoints with the F5 key press. This button continues program
execution until it encounters the next breakpoint. Once the program flow comes to the
breakpoint, the PHP interpreted becomes paused, and you can review the state of the program.
You can see the complete list of breakpoints you have set in the Breakpoints window (see figure
B.9). The Breakpoints window is located in the bottom part of NetBeans window. In this window
you can add new breakpoints or unset breakpoints that have already been set.
Watching Variables
When the PHP interpreter is paused, you can conveniently watch the values of PHP variables.
A simple way to browse a variable is just positioning the mouse cursor over the variable name
inside of the code and waiting for a second. If the variable value can be evaluated, it will be
displayed inside of a small pop up window.
Another way to watch variables is through the Variables window (shown in figure B.10), which
is displayed in the bottom part of NetBeans window. The Variables window has three columns:
Name, Type and Value.
457
Mainly, you will be faced with three kinds of variables: super globals, locals and $this:
Super global variables are special PHP variables like $_GET, $_POST, $_SERVER, $_COOKIES
and so on. They typically contain server information and parameters passed by the web
browser as part of HTTP request.
Local variables are variables living in the scope of the current function (or class method).
For example, in the Hello World application, if you place a breakpoint inside of the
IndexController::aboutAction(), the variable $zendFrameworkVer will be a local variable.
$this variable points to the current class instance, if the current code is being executed in
context of a PHP class.
Some variables can be expanded (to expand a variable, you need to click on a triangle icon
to the left of variables name). For example, by clicking and expanding $this variable, you can
watch all fields of the class instance. If you expand an array variable, you will be able to watch
array items.
Using the Variables window it is possible not only to watch variables value, but also to change
the value on the fly. To do that, place your mouse cursor over the value column and click over
it. The edit box appears, where you can set the new value of the variable.
Call Stack
The call stack displays the list of nested functions whose code is being executed at the moment
(shown in the figure B.11). Each line of the call stack (also called a stack frame) contains the full
name of the class, the name of the method within the class and line number. Moving down the
stack, you can better understand the current execution state of the program.
458
For example, in figure B.11, you can see that currently the IndexController::aboutAction() is
being executed, and this method was in turn called by the AbstractActionController::onDispatch()
method, and so on. We can walk the call stack until we reach the index.php file, which is the top
of the stack. You can also click a stack frame to see the place of the code that is currently being
executed.
Debugging Options
NetBeans allows you to configure some aspects of the debuggers behavior. To open the Options
dialog, select menu Tools->Options. In the dialog that appears, click the PHP tab, and in that tab,
select Debugging subtab (figure B.12).
459
You typically do not change most of these options, you just need to have an idea of what they
do. These are the following debugging options:
The Debugger Port and Session ID parameters define how NetBeans connects to XDebug.
By default, the port number is 9000. The port number should be the same as the debugger
port you set in php.ini file when installing XDebug. The session name is by default
netbeans-xdebug. You typically do not change this value.
The Stop at First Line parameter makes the debugger to stop at the first line of your
index.php file, instead of stopping at the first breakpoint. This may be annoying, so you
may want to uncheck this option.
The Watches and Balloon Evaluation option group is disabled by default, because these
may cause XDebug fault. You can enable these options only when you know what you are
doing.
The Maximum Depth of Structures parameter sets whether nested structures (like
nested arrays, objects in objects, etc.) will be visible or not. By default, the depth is
set to 3.
The Maximum Number of Children option defines how many array items to display
in Variables window. If you set this to, say 30, you will see only the first 30 items
even when the array has more than 30 items.
The Show Requested URLs option, when enabled, displays the URL which is currently
being processed. It prints the URL to an Output window.
460
The Debugger Console option allows to see the output of PHP scripts being debugged. The
output is shown in the Output window. If you plan to use this feature, it is recommended to
add output_buffering = Off parameter in [xdebug] section of your php.ini file, otherwise
the output may appear with delays.
Profiling
When your site is ready and working, you are typically interested in making it as fast and
performing as possible. XDebug provides you with an ability to profile your web site. Profiling
means determining which class methods (or functions) spend what time to execute. This allows
you to determine the bottle neck places in your code and address the performance issues.
For each HTTP request, the XDebug extension measures the amount of time a function
executes, and writes the profiling information to a file. Typically, the profiling info files are
placed into the system temporary directory (in Linux, to /tmp directory) and have names like
xdebug.out.<timestamp>, where the <timestamp> placeholder is the timestamp of the HTTP
request. All you have to do is to open a profiling file and analyze it.
To enable XDebug profiler, you should set the following XDebug configuration
parameter in your php.ini file:
xdebug.profiler_enable = 1
Unfortunately, NetBeans for PHP does not have an embedded tool for visualizing the profiling
results. Thats why you need to install a third-party visualizer tool. Below, we will provide
instructions on how to install a simple web-based tool named Webgrind. Webgrind can work
on any platform, because this tool itself is written in PHP.
Webgrinds installation is very straightforward.
First, you need to download webgrind from its project page and unpack it to some folder. In
Linux, you can do this with the following commands:
cd
wget https://webgrind.googlecode.com/files/webgrind-release-1.0.zip
unzip webgrind-release-1.0.zip
The commands above will change your working directory to your home directory, then will
download the Webgrind archive from the Internet, and then unpack the archive.
Next, you need to tell the Apache web server where to find Webgrind files. This means you need
to configure a separate virtual host. We have already learned about virtual hosts in Appendix A.
Do not forget to restart Apache web server after you have configured the virtual host.
Finally, open Webgrind in your browser by navigating to the URL of your Webgrind install. For
example, if you configured the virtual host to listen on port 8080, enter http://localhost:8080
https://code.google.com/p/webgrind/
461
in your browsers navigation bar and press Enter. The Webgrind web page should appear (see
figure B.13):
At the top of the Webgrind page, you can select the percentage of the heaviest function calls
to show (figure B.14). By default, it is set to 90%. Setting this to a lower percentage will hide the
functions called less often.
The drop-down list to the right of percent field allows to select the profiling data file to analyze.
By default, it is set to Auto (newest), which forces Webgrind to use the file with the most recent
timestamp. You may need to select another file, for example, if your web pages use asynchronous
AJAX requests.
The right-most drop-down list allows to set the units which should be used for measuring the
data. Possible options are: percent (default), milliseconds and microseconds.
462
When you have selected the percentage, file name and units, click the Update button to let
Webgrind to visualize the data for you (the calculation may take a few seconds). As the
calculation finishes, you should be able to see the table of function calls, sorted in descending
order by function weight. The heaviest functions will be displayed at the top.
The table has the following columns:
The first column (Function), displays the class name followed by method name (in case of
a method call) or function name (in case of a regular function).
The second column contains the paragraph icons, which can be clicked to open the
corresponding PHP source file that function is defined in the web browser.
Invocation Count column displays the number of times the function was called.
Total Self Cost column shows the total time it took to execute the built-in PHP code in the
function (excluding the time spent on executing other non-standard functions).
Total Inclusive Cost column contains the total execution time for the function, including
built-in PHP code and any other user functions called.
Clicking a column header allows to sort data in ascending or descending order.
You can click the triangle icon next to a function name to expand a list of function invocations.
This list allows you to see who called this function and what the amount of time spent is, and
contains the following columns:
Calls is the parent functions or class methods invoking this (child) function;
Total Call Cost is the total time executing this function, when called from the parent
function;
Count - number of times the parent calls the child function.
The coloured bar at the top of the page displays the contribution of different function types:
Please note that the profiler creates a new data file in your /tmp directory for each
HTTP request to your web site. This may cause disk space exhaustion, which can
be fixed only by rebooting your system. So, when youve finished profiling your
application, it is recommended to disable the profiling by editing the php.ini file,
commenting the xdebug.profiler_enable parameter as follows, and then restarting
the Apache web server.
;xdebug.profiler_enable = 0
463
Summary
In this appendix, weve learned how to use NetBeans IDE to run the web site and debug it
in interactive step-by-step mode. To be able to run a web site, you first need to edit the sites
properties (run configuration).
To debug the site, you need to have the XDebug PHP extension installed. When you debug your
web site in NetBeans, the PHP engine pauses program execution at every line where you set a
breakpoint. You see the debugging information (like local variables and call stack) in NetBeans
window in graphical form.
Along with debugging, XDebug extension also provides the ability to profile web sites. With
profiling, you see how much time was spent for execution of a certain function or class method.
This allows you to determine the bottle necks and performance issues.
Lets look in more details at the files stored inside the APP_DIR/public directory and its
subdirectories (figure C.1).
The css directory contains the CSS stylesheets:
The bootstrap.css and bootstrap.min.css files are the usual and minified versions of
Bootstrap, respectively.
The bootstrap-theme.css is the optional Bootstrap theme file for a visually enhanced
experience. The bootstrap-theme.min.css is its minified version.
The style.css file is the stylesheet that can be used and extended by you to define your own
CSS rules which will be applied on top of Bootstrap rules. This way you can customize the
appearance of your web application.
465
The html5shiv.js and respond.min.js files are actually not part of either Bootstrap or
jQuery. They are used for compatibility with older versions of Internet Explorer web
browser. The first one enables the use of HTML5 elements and provides basic HTML5
styling, and the latter one enables the responsive web designs.
After the concatenation and minification, the JavaScript code is difficult to read and debug. A MAP file (source map) allows to restore the
minified file back to its usual state.
https://github.com/aFarkas/html5shiv
https://github.com/scottjehl/Respond
466
Grid System
In most web sites, content is required to be organized in a table-like structure having rows and
columns. In figure C.2, you can see an example layout of a typical web site: it has the header
block with a logo, the sidebar at the left, page content area in the middle, the ads bar at the right,
and the footer at the bottom of the page. These blocks are arranged in a grid, although grid cells
have unequal width (some cells can span several columns).
Bootstrap provides a simple layout grid system to make it easy to arrange content on your pages
in rows and columns.
Each row consists of up to 12 columns (figure C.3). Column width is flexible and depends on
the width of the grid container element. Column height may vary depending on the height of
the content of the cell. The space between columns is 30 pixels (15 pixels padding at both sides
of the column).
Columns can be spanned, so a single cell takes the space of several columns. For example, in
figure C.3, the upper grid row consists of 12 columns, and each cell spans a single column. In the
bottom row, the first cell spans 2 columns, the second and the third ones span 4 columns each,
and the fourth cell spans 2 columns (in total we have 12 columns).
Why does Bootstraps grid consist of only 12 columns?
Probably because 12 columns are enough for most web sites. If you have more finegrained grid with lots of columns, it would be more difficult to compute column spans
without the calculator. Fortunately, Bootstrap allows for customizing the count of
columns per row, so you can have as many columns as you wish.
You are not required to put exactly 12 columns in a row, there may be fewer columns. If you have fewer columns, the space to the right of
the last column will be empty.
467
To add columns, you use <div> elements with CSS class names varying from .col-md-1 to
.col-md-12. The number in the class name specifies how many columns each grid cell will span:
<div class="container">
<div class="row">
<div class="col-md-1">Cell 1</div>
<div class="col-md-5">Cell 2</div>
<div class="col-md-6">Cell 3</div>
</div>
</div>
In the example above, we have three cells. The first cell has a width of 1 (it uses the .col-md-1
class), the second cell spans 5 grid columns (class .col-md-5) and the third cell spans 6 columns
(class .col-md-6).
As another example, lets define the layout that we saw in figure C.2. The layout has the header
(logo spans 3 columns), the main content area (spans 7 columns), the sidebar (spans 3 columns),
the advertisements bar (2 columns) and the footer. To produce this layout, we can use the
following HTML code:
<div class="container">
<!-- Header -->
<div class="row">
<div class="col-md-3">Logo</div>
<div class="col-md-9"></div>
</div>
<!-- Body-->
<div class="row">
<div class="col-md-3">Sidebar</div>
<div class="col-md-7">Page Content</div>
<div class="col-md-2">Ads</div>
</div>
<!-- Footer -->
468
<div class="row">
<div class="col-md-12">Page Footer</div>
</div>
</div>
Offsetting Columns
In real web pages, sometimes the grid needs to contain empty holes. You can define such
holes by offsetting cells to the right with the help of CSS classes named from .col-md-offset-1
to .col-md-offset-12. The number in the class name specifies how many columns should be
skipped.
For example, look at figure C.4:
The grid above has three cells, the latter two cells are offsetted to the right, making empty holes.
To define the grid like in figure C.4, you can use the following HTML code:
1
2
3
4
5
6
7
<div class="container">
<div class="row">
<div class="col-md-2">Cell 1</div>
<div class="col-md-4 col-md-offset-2">Cell 2</div>
<div class="col-md-2 col-md-offset-2">Cell 3</div>
</div>
</div>
Nesting Grids
You can create complex page layouts by nesting grids (for example, look at figure C.5). To nest
your content, you add a new <div> element containing .row class, and set of .col-md-* columns
within an existing .col-md-* column.
To produce the grid as shown in figure C.5, you can use the following HTML code:
469
<div class="container">
<div class="row">
<div class="col-md-2">Cell 1</div>
<div class="col-md-8">
<!-- Nested grid -->
<div class="row">
<div class="col-md-4">Cell 21</div>
<div class="col-md-4">Cell 22</div>
</div>
<div class="row">
<div class="col-md-4">Cell 23</div>
<div class="col-md-4">Cell 24</div>
</div>
</div>
<div class="col-md-2">Cell 3</div>
</div>
</div>
In the example above, we defined the grid consisting of three cells (denoted by gray color): the
first cell spanning 2 columns, the second cell spanning 8 columns and the third cell spanning
2 columns. Then we put the nested grid rows inside of the second cell. Because the parent cell
spans 8 columns, the child grid consists of 8 columns, too.
This adaptation is performed in two ways. The first way is that the column width within the grid
is flexible. For example, if you increase the size of the browser window, the grid will be scaled
accordingly to fill the whole space.
But what will happen if your web page is too wide for the display? To see the hidden part, the site
visitor will need to scroll it to the right. For mobile phones and other low-resolution devices this
is not a good approach. Instead, it would be better for the grid to become stacked below some
screen width. When the grid is stacked, its rows are transformed, making cells to be positioned
one below another (see figure C.6 for example).
To better control when the grid becomes stacked, Bootstrap provides you with additional
CSS classes: .col-xs-1 to col-xs-12 (the xs abbreviation means extra-small devices, or
470
phones), .col-sm-1 to .col-sm-12 (sm stands for small devices, or tablets), and .col-lg-1
to .col-lg-12 (large devices, or wide displays). These classes can be used together with the
.col-md-1 .col-md-12 classes, that we already used (the md abbreviation means medium
devices, or desktops).
For example, .col-md-* classes define the grid which will become stacked when the screen
is below 992 pixels wide, and horizontal for wider screens. The .col-sm-* can be used to make
the grid stacked below 768 pixel screen width, and horizontal above this point. The .col-xs-*
class makes the grid always horizontal, not depending on the screen width.
Table C.1 provides the summary of available grid classes and their breakdown page width.
Table C.1. CSS classes for defining layout grid
Class name
Breakdown width
.col-xs-*
.col-sm-*
.col-md-*
.col-lg-*
<768px
>=768px
>=992px
>=1200px
Bootstraps grid system greatly simplifies the positioning of elements on a web page.
However, using the grid system is not mandatory. For example, sometimes you may
need a much more complex layout, and the simple grid system will be insufficient. In
such a case, you can create and use your custom layout by using <table> or <div>
HTML elements.
471
Navigation Bar
Navigation bar is usually positioned on top of your web site and contains the links to main pages,
like Home, Download, Support, About, etc. Twitter Bootstrap provides a nice visual style for the
navbar (see figure C.7 for example):
As you can see from the figure above, a navbar typically has the header (brand name of your
site can be placed here) and the links to main pages. To put a navbar on your page, you use the
following HTML code:
1
2
3
4
5
6
7
8
9
10
11
In line 1 above, we used the <nav> element, which contains all the navigation bar information.
The associated CSS class .navbar is defined by Bootstrap and provides the base navigation bars
appearance. The .navbar-default CSS class specifies the default theme for the navigation bar.
The optional role attribute is an HTML attribute allowing to annotate the page elements with
machine-extractable semantic information about the purpose of an element. In this example, the
attribute tells that the <nav> element is used for navigation.
In lines 2-4, we define the navbar header area, which contains the Hello World hyperlink.
The brand hyperlink typically points to the main page of your site. The hyperlink has the
.navbar-brand class that visually enhances the text.
In lines 5-10, we specify the navigation links for the Home, Download, Support and About pages.
These links are organized inside an <ul> unordered list element. The element has CSS classes
.nav and .navbar-nav that place list items in line and provide the hover item state.
Dropdown Menu
With Bootstrap navigation bar, it is possible to use the dropdown menu as a navigation item.
For example, if the Support section of your site can be subdivided into Documentation and Help
pages, these can be implemented as a dropdown menu (see figure C.8).
http://www.w3.org/TR/xhtml-role/
472
You define the dropdown menu by replacing the Support list item from the previous example in
the following way:
1
2
3
4
5
6
7
8
9
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Support <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="#">Documentation</a></li>
<li><a href="#">Help</a></li>
</ul>
</li>
In the code above, we use the <li> element with CSS class .dropdown that indicates the dropdown
menu (line 1). In lines 2-4, the <a> element defines the hyperlink to show when the menu is hidden
(the Support text is shown followed by the triangle caret).
When the site user clicks the hyperlink, the dropdown menu (lines 5-8) appears. The <ul>
unordered list element with class .dropdown-menu defines its visual appearance. The dropdown
menu contains two items: the Documentation and Help hyperlinks.
Collapsible Navbar
As with the grid system, the navbar component supports different types of screen resolutions.
On low-resolution devices, the navbar can be collapsed, as shown in figure C.9.
As you can see, in the collapsed mode, only the navbar header is displayed, and the three
horizontal bars at the right denote the Toggle button. Clicking the button would expand the
hidden navbar items.
You define the collapsible navigation bar as shown in the example below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
473
Above in lines 3-12, we define the navbar header which will be displayed independently on screen
resolution. The header contains the Toggle button with three horizontal bars and description text
Toggle navigation.
The collapsible part of the menu can be seen in lines 15-30. In this area, we put our navigation
links and the dropdown menu items.
Inverse Navbar Style
The navigation bar can be displayed using two standard themes: the default theme (we saw it
above), and the inverse theme. The inverse theme makes the navbar elements be displayed in dark
colors (figure C.10). You probably saw such an inverse navbar in the Zend Skeleton Application
demo.
474
The inverse theme is defined by simply replacing the .navbar-default class of the <nav> element
by the .navbar-inverse class:
<nav class="navbar navbar-inverse" role="navigation">
...
</nav>
Breadcrumbs
Breadcrumbs is a useful interface component which can be used together with the navbar to give
the site visitor an idea of his current location within the site (figure C.11).
In the figure above, we have an example breadcrumbs for the documentation system of our site.
Because the documentation pages can have deep nesting level, the breadcrumbs tell the user
which page he is visiting right now so the user will not get lost and will be able to return to the
page he visited previously, and to the upper-level pages.
To define the breadcrumbs, you use the ordered list <ol> element with the .breadcrumb CSS
class (see an example below):
<ol class="breadcrumb">
<li><a href="#">Home</a></li>
<li><a href="#">Support</a></li>
<li class="active">Documentation</li>
</ol>
Pagination
The pagination component is useful when you have a long list of items for display. Such a long
list, if displayed on a single page, would require the user to scroll the page down several times to
see the bottom of the list. To improve user experience, you would break the output into pages,
and use the pagination component for navigation between the pages (figure C.12):
To define the pagination like in figure above, use the following HTML code:
475
<ul class="pagination">
<li><a href="#">« Newest</a></li>
<li><a href="#">< Newer</a></li>
<li><a href="#">1</a></li>
<li><a href="#">2</a></li>
<li><a href="#">3</a></li>
<li><a href="#">4</a></li>
<li><a href="#">5</a></li>
<li><a href="#">Older ></a></li>
<li><a href="#">Oldest »</a></li>
</ul>
To create the buttons like in the figure above, use the following HTML code:
<p>
<button type="button" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-default">Cancel</button>
</p>
In the code above, we use the .btn CSS class to assign the button its visual style. Additionally,
we use the .btn-primary class for the Save button (which is typically the primary button on a
form), or the btn-default class for a usual non-primary button Cancel.
To better express the meaning of a button, Bootstrap provides you with several additional
button classes: .btn-success (for buttons applying some change on the page), .btn-info (for
informational buttons), .btn-warning (for buttons that may have an undesired effect), and
.btn-danger (for buttons that may lead to irreversible consequences). For an example of using
these button styles, look at the code below:
<p>
<button
<button
<button
<button
<button
<button
</p>
type="button"
type="button"
type="button"
type="button"
type="button"
type="button"
class="btn
class="btn
class="btn
class="btn
class="btn
class="btn
btn-default">Default</button>
btn-primary">Primary</button>
btn-success">Success</button>
btn-info">Info</button>
btn-warning">Warning</button>
btn-danger">Danger</button>
476
Bootstrap includes 180 icons (called Glyphicons) that you can use together with your buttons,
dropdown menus, navigation links, etc. To add an icon on a button, you can use the code like
the one below:
<p>
<button type="button" class="btn btn-default">
<span class="glyphicon glyphicon-plus"></span> Create
</button>
<button type="button" class="btn btn-default">
<span class="glyphicon glyphicon-pencil"></span> Edit
</button>
<button type="button" class="btn btn-default">
<span class="glyphicon glyphicon-remove"></span> Delete
</button>
</p>
In the code above, we defined a simple toolbar containing three buttons: Create, Edit and Delete.
We placed an icon on each button with the help of <span> element. The <span> element should
have two classes: the .glyphicon class is common for all icons; the second class represents the
icon name. In the example above, we used .glyphicon-plus class for the Create button, the
.glyphicon-pencil for Edit button, and .glyphicon-remove for Delete button. The result of
our work is presented in figure C.15.
You can vary button sizes by specifying the .btn-lg class for large buttons, btn-sm for small
buttons, or .btn-xs class for extra-small buttons. For example, in figure C.16, a large Download
button is presented.
To define such a button, you can use the following HTML code:
477
Customizing Bootstrap
To finish the introduction to Twitter Bootstrap, we will describe about how to modify some
aspects of Bootstrap framework. You can customize the Bootstrap look and feel using the
Customize page of the Bootstrap web site (figure C.17).
On the Customize page you can choose which Bootstrap source files to include into the
concatenated resulting file bootstrap.css. If you dont need some functionality, you can exclude
http://getbootstrap.com/customize/
478
it from the resulting file, thus reducing the network traffic and page load time. You can also
remove some unused JavaScript code components from the resulting bootstrap.js file.
Additionally, you can choose different CSS parameters like background color, base text color and
font family, and so on. There are more than a hundred customizable parameters available.
CSS customization is possible, because Bootstrap source files are stored in LESS format, which allows to define variable parameters (like @bodyBackground or @textColor).
Once the parameters are defined, the LESS files are compiled into usual CSS files,
minified and made available for downloading.
When you have finished with tuning parameters, you can scroll the Customize page down
and press the Compile and Download button. As a result, the bootstrap.zip archive will be
downloaded, which contains all the customized Bootstrap files (both usual and minified CSS
and JS files and glyphicons fonts).
Summary
Twitter Bootstrap is a CSS framework developed to make designing your web pages easier. It
provides the default nice-looking style for typography, tables, forms, buttons, images and so on,
so you can create a professionally looking page in a minute.
The grid system provided by the Bootstrap allows to arrange elements on your web page in a
grid with rows and columns. The grid adapts to different screen resolutions, making your page
equally well-readable on mobile phones, tablets, desktops and wide screens.
Twitter Bootstrap also provides useful web interface components like dropdown menus, navigation bars, breadcrumbs, etc. These components are made interactive by the JavaScript extensions
bundled with the framework.
Bootstrap is shipped with Zend Skeleton Application, so you can start using it out of the box
or, alternatively, you can download the newest version of Bootstrap from the projects page and
customize it as you wish.
LESS is a dynamic stylesheet language extending standard CSS with features like variables, mixins (embedding all the properties of a CSS
class into another CSS class), code block nesting, arithmetic operations, and functions.
Appendix D. Introduction to
Doctrine
In this appendix, we provide overview of the Doctrine library, such as its architecture and
components. Since in this book, we concentrate mainly on Doctrines Object Relational Mapper
(ORM) component, reading this appendix may give you the bigger picture of other Doctrine
capabilities.
Relational Databases
In a relational database, you have a collection of tables consisting of records. A record may have
one or several columns. A record (or several records) of a table may be linked to a record (or
several records) of another table, thus forming a relation between data.
For example, assume you have a blog web site whose database contains two tables: the post
table and the comment table. The post table would have columns named id, title, content,
author, date_created; and the comment table would have columns named id, author, content,
and date_created. The post table is related to comment table as one-to-many, because one post
has zero or more (many) comments, while a certain comment may belong to a single post only.
Graphically, the above mentioned tables, their columns and relationship are shown in figure D.1
below.
Figure D.1. Relation between tables. Single post has many comments
480
On the market, there is a number of major relational databases. Among them: SQLite, MySQL,
PostgreSQL, Oracle, Microsoft SQL Server etc.
Each database system has its own features specific to that DBMS and which are not part of other
systems. For example:
SQLite is designed as an embed extension of PHP engine and doesnt require installation,
however it works well for simple sites only;
MySQL is a free system which is very simple in installation and administration and good
for using in systems varying from small to middle scale;
Commercial Oracle DBMS is mainly targeted on large-scale systems and has sophisticated
administrative tools;
PostgreSQL supports indefinitely large databases and can be considered as an open-source
replacement of Oracle.
Doctrine library is designed to work with all major databases using a unified programming
interface. This programming interface is implemented in two levels:
1. At the lower level, Doctrine provides the unified mechanism for building SQL queries to
any supported relational database and manipulating database schema. This mechanism is
implemented in the Database Access Layer (DBAL) component.
2. At the higher level, the Object Relational Mapper (ORM) component of Doctrine provides
an ability to query and manage database data in object-oriented way, by mapping the
tables to PHP classes. This component also provides its custom database query language
called DQL allowing to build queries in object-oriented style.
Typically, you use the API provided by high-level ORM component. At the same time, you can
easily work with lower-level DBAL component, if you find that more suitable for your particular
needs.
Doctrine is database-agnostic. In theory, when you use Doctrine you are able to abstract
of database type and switch between databases more easily than when you use your
database-dependent solution.
481
Doctrine library is designed to work with all major relational database systems that use
SQL language, but it is obvious that it supports only some subset of their functionality
and SQL language capabilities.
Doctrine is built on top of PHP PDO extension (and other database-specific PHP extensions, like
sqlite, mysqli, oci8, etc.). Those extensions provide drivers for all major relational database
systems. You specify which driver to use when configuring a database connection.
If you are not familiar with SQL, a good point for learning its syntax is W3Schools
Tutorials.
Since the Object Relational Mapper component of Doctrine is designed to work with objects
instead of tables, it provides its own object-oriented query language called DQL. It is similar
to SQL in sense that it allows to write and execute queries to database, but result of a query is
an array of objects rather than an array of table rows.
NoSQL Databases
In contrast to a relational database system, a NoSQL database system - as its name assumes uses a not-only-SQL method of accessing the data. This means that each NoSQL system may
provide its own custom methods and API for accessing and manipulating the data. Technically,
NoSQL databases can be divided in the following groups:
Document Store. A document database operates the concept of documents and their
fields. This is useful, for example, if you have an hierarchical document tree in a content
management (CMS) system. Documents are addressed in the database via a unique key
that represents that document. One of the other defining characteristics of a documentoriented database is that, beyond the simple key-document lookup that you can use to
retrieve a document, the database will offer an API or query language that will allow
retrieval of documents based on their contents.
Column Store. Frequently used in web indexing. A column-oriented DBMS is a database
management system that stores data tables as sections of columns of data rather than as
rows of data. In comparison, most relational DBMSs store data in rows. This columnoriented DBMS has advantages for data warehouses, customer relationship management
(CRM) systems, and library card catalogues, and other ad hoc inquiry systems where
aggregates are computed over large numbers of similar data items.
Key-Value Store. This is the simplest data storage using unique keys for accessing certain
data. Such database systems provide a simple key-value lookup mechanism.
and others.
Doctrine provides support only to the Document Store subset of the NoSQL database
systems. Column store and key-value store database systems typically have very
specific field of applications, and not covered by Doctrine.
http://www.w3schools.com/sql/default.asp
482
Document Databases
Doctrine supports a number of NoSQL document store databases: MongoDB, CouchDB,
OrientDB and PHPCR.
For example, in a blog web site, you would have a document named post and a document
named comment. The post document would have fields named id, title, content, author,
date_created; and the comment document would have fields named id, author, content and
date_created. This is very similar to the tables you would have in a relational database.
In this book, we do not address the Doctrine-provided API to the NoSQL document
databases. If you want to learn about these capabilities, please refer to the corresponding sections of Doctrine project documentation.
Doctrine Architecture
The Doctrine Project consists of several libraries (components). Each Doctrine component is
distributed as a Composer-installable package and registered in Packagist.org catalogue. This
is very similar to the way that Zend Framework 2 uses for installing its components.
At the moment of writing this book, the latest version of Doctrine is v.2.4.
Here we will provide you with a brief description of Doctrine library architecture to let you a
general idea of its capabilities.
483
Doctrine ORM component uses the so called Data Mapper pattern. This pattern tells
that a database table can be represented as a PHP entity class. The database in this
pattern is considered as some kind of repository (storage of entities). When you retrieve
an entity from the repository, an SQL query is performed internally, and an instance
of the PHP entity class is constructed and its properties are filled with data. Vice versa,
when you save the entity to repository, the values of its properties are read from the
entity and saved to database table by an SQL query.
Figure D.2. Doctrine components designed for working with relational databases
By analogy with ZF2 components, Doctrine component names consist of two parts: the vendor
name (Doctrine) and the component name (e.g. Common). Below, you can find the list
of Doctrine components together with their Composer-installable package names and brief
descriptions:
Doctrine\Common. Common Library for Doctrine projects. This component contains
commonly used functionality. Its Composer-installable package name is doctrine/common.
Doctrine\Annotations. Docblock Annotations Parser. Its Composer-installable package
name is doctrine/annotations.
http://en.wikipedia.org/wiki/Data_mapper_pattern
484
Doctrine\Inflector. Common String Manipulations with regard to casing and singular/plural rules. Its Composer-installable package name is doctrine/inflector.
Doctrine\Lexer. Base library for a lexer that can be used in Top-Down, Recursive Descent
Parsers. Its Composer-installable package name is doctrine/lexer.
Doctrine\Cache. Caching library offering an object-oriented API for many cache backends. Its Composer-installable package name is doctrine/cache.
Doctrine\DBAL. Database Abstraction Layer. This is a lightweight and thin runtime layer
around a PDO-like API and a lot of additional, horizontal features like database schema
introspection and manipulation through an object oriented API. Its Composer-installable
package name is doctrine/dbal.
Doctrine\Collections. Collections Abstraction library. Its Composer-installable package
name is doctrine/collections.
Doctrine\ORM. Object-Relational-Mapper for PHP. This is a Doctrine component providing a way to work with entity models in object-oriented way instead of raw SQL queries.
Its composer installable package name is doctrine/orm.
Doctrine\Migrations. Database Schema migrations using Doctrine DBAL. Provide a
consistent way to manage database schema and update it. Its composer installable package
name is doctrine/migrations.
Doctrine\DataFixtures. Data Fixtures for all Doctrine Object Managers. Provides a
framework for making database fixtures. Its composer installable package name is doctrine/data-fixture
Since Doctrine uses PHP autoloading and PSR-0 standard, classes belonging to certain component live in that components namespace. For example, the EntityManager class belonging to
Doctrine\ORM component, lives in Doctrine\ORM namespace.
485
Doctrine\MongodbODM (Object Document Mapper) provides a way to map NoSQL documents to PHP entity models. Its Composer-installable package name is doctrine/mongodb-odm.
Figure D.3. Doctrine components designed for working with document databases
Doctrine\MongoODMModule is Zend Framework 2 Module that provides Doctrine MongoDB ODM functionality. It serves for easy integration with ZF2. Its Composer-installable
package name is doctrine/doctrine-mongo-odm-module.
Doctrine\CouchDB component provides Simple API that wraps around CouchDBs HTTP
API. Its Composer-installable package name is doctrine/couchdb.
Doctrine\CouchDB component is CouchDB Document Object Mapper. It is analogous to
Doctrine ORM in sence that it provides the way to access database in object oriented way.
Its Composer-installable package name is doctrine/couchdb.
Doctrine\OrientdbODM is a set of PHP libraries in order to use OrientDB from PHP. Its
Composer-installable package name is doctrine/orientdb-odm.
Doctrine\PhpcrODM is Object-Document-Mapper for PHPCR. Its Composer-installable
package name is doctrine/phpcr-odm.
Summary
In this appendix, weve provided the overview of Doctrine library architecture and components.
Doctrine is a large project consisting of multiple components mainly targeted on data persistence.
486
On the market, there are two big groups of database management systems: traditional relational
databases and so called NoSQL databases. Although most relational databases use SQL language
for querying and manipulating data, each particular database system has its own specific
features. The same thing can be seen with NoSQL databases, where each system provides its own
custom method for accessing data. Doctrine is designed to work with data in database-agnostic
way by providing sophisticated abstraction layers.
The most useful component of Doctrine, Object Relational Mapper (ORM) is designed to let the
developer an ability to work with data in object oriented way. This is when instead of writing an
SQL query, you load an entity object (or an array of entity objects) from a repository. With this
approach, a database table is mapped to a PHP class (also called an entity), and a record from
that table is mapped to an instance of that entity class.
http://olegkrivtcov.wordpress.com/