Yi I 2 For Beginners
Yi I 2 For Beginners
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.
2014 - 2015 Bill Keck
This is book is dedicated to anyone who has taken the time to help someone else learn something
about programming, whether it is through an online article, blog, community forum, or book. As I
have benefited from the help of others, so too, I wish to contribute
I would also like to thank the core Yii 2 team, starting with Yii founder Qiang Xue, who, with his
creation of Yii 2, has brought something beautiful and useful into the world. Also, many thanks to
the tireless efforts of Samdark, Cebe, Orey, and Kartik, who take the time to help people like me, and
others like me, who have a passion for programming, but sometimes need a little help filling in the
details.
And finally, thank you to my wife and kids, just because without them, I would not endeavor to reach
so high, even if Im only able to grasp so little. In the end, its the dream of accomplishing something
that drives me, and it is only with their love and support that Im able to do this at all.
Contents
Chapter One: Introduction . . . . . . . . . .
Introduction . . . . . . . . . . . . . . . . .
Features . . . . . . . . . . . . . . . . . . .
What Makes The Yii 2 Framework Special?
Upsides . . . . . . . . . . . . . . . . . . .
Downsides . . . . . . . . . . . . . . . . . .
Why I chose Yii 2 . . . . . . . . . . . . . .
Other Options . . . . . . . . . . . . . . . .
Yii 2 Arrives . . . . . . . . . . . . . . . . .
Gii . . . . . . . . . . . . . . . . . . . . . .
DB-First Approach . . . . . . . . . . . . .
MySql . . . . . . . . . . . . . . . . . . . .
Improved Workflow . . . . . . . . . . . . .
Minimum PHP Skills . . . . . . . . . . . .
Tools You Will Need . . . . . . . . . . . .
Errata . . . . . . . . . . . . . . . . . . . .
Contact Bill Keck . . . . . . . . . . . . . .
Summary . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
1
2
3
3
3
4
4
5
5
5
6
6
7
9
10
10
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
12
12
13
13
14
15
16
18
18
19
20
20
21
21
CONTENTS
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
22
23
24
24
25
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
26
26
28
29
29
30
30
31
32
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
34
35
37
45
46
46
49
50
51
54
56
56
60
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
61
61
62
63
63
64
64
65
66
68
69
72
73
CONTENTS
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
74
76
78
80
81
83
85
85
87
88
96
101
102
104
116
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
117
117
127
132
134
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
135
136
137
138
139
139
143
143
144
145
148
152
152
153
159
162
164
165
167
CONTENTS
CRUD . . . . . . . . . . . . . . . . .
Profile Controller . . . . . . . . . . .
Profile Search . . . . . . . . . . . . .
_search . . . . . . . . . . . . . . . . .
_form . . . . . . . . . . . . . . . . .
Index . . . . . . . . . . . . . . . . . .
View . . . . . . . . . . . . . . . . . .
Create . . . . . . . . . . . . . . . . .
Update . . . . . . . . . . . . . . . . .
Modifying Profile Controller & Views
Modifying the Profile Controller . . .
Index Action . . . . . . . . . . . . . .
View Action . . . . . . . . . . . . . .
Create Action . . . . . . . . . . . . .
Update Action . . . . . . . . . . . . .
Delete Action . . . . . . . . . . . . .
FindModel Action . . . . . . . . . . .
Modifying the Profile Views . . . . .
View.php . . . . . . . . . . . . . . . .
Gender . . . . . . . . . . . . . . . . .
Form Partial . . . . . . . . . . . . . .
Create . . . . . . . . . . . . . . . . .
Update . . . . . . . . . . . . . . . . .
Site Layout . . . . . . . . . . . . . . .
Profile Link . . . . . . . . . . . . . .
DatePicker . . . . . . . . . . . . . . .
Summary . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
169
171
171
171
171
171
172
172
173
173
174
176
178
180
182
184
185
186
186
192
193
196
196
197
201
202
204
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
206
207
208
208
209
214
217
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
218
218
218
219
220
226
228
.
.
.
.
.
.
.
CONTENTS
HTML Helper . . . . . . . . .
Collapse Widget . . . . . . . .
Modal Widget . . . . . . . . .
Alert Widget . . . . . . . . . .
Font-Awesome . . . . . . . .
Asset Bundle . . . . . . . . . .
Add Font-Awesome to Layout
Summary . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
229
235
237
237
238
239
241
248
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
249
253
258
258
258
263
266
267
268
270
272
273
275
296
302
303
306
307
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
308
308
325
333
373
374
379
399
402
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
404
404
405
406
406
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
CONTENTS
Slugs . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Sluggable Behavior . . . . . . . . . . . . . . . . . . . . .
Slug Column . . . . . . . . . . . . . . . . . . . . . . . . .
Drop old Faqs and Create New Ones . . . . . . . . . . . .
Add Url Manager Rules . . . . . . . . . . . . . . . . . . .
Modify View Action on FaqController . . . . . . . . . . .
Modify Create and Update Actions on Backend Controller
Change Gridview Action Column URL . . . . . . . . . .
Summary . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
408
408
409
410
411
411
413
414
421
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
423
423
424
424
425
426
426
427
429
435
437
439
440
441
443
444
445
445
447
451
461
461
465
470
474
476
482
485
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
486
487
487
487
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
CONTENTS
Status . . . . . . . . . . . .
ValueHelpers . . . . . . . .
PermissionHelpers . . . . .
RecordHelpers . . . . . . . .
Database Changes . . . . . .
Extra ValueHelpers . . . . .
LoginForm Model . . . . . .
PasswordResetRequestForm
UserSearch . . . . . . . . . .
ProfileSearch . . . . . . . .
Main.php . . . . . . . . . .
Index.php . . . . . . . . . .
Troubleshooting . . . . . . .
Summary . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
487
487
488
488
488
488
489
489
490
490
490
490
490
491
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
492
492
492
493
494
494
500
503
506
509
511
512
513
515
521
522
524
525
525
525
525
526
532
533
536
543
544
545
CONTENTS
Modify Views . . . . . . . . . . .
Modify View . . . . . . . . . . . .
Modify Update . . . . . . . . . .
Modify _form . . . . . . . . . . .
Modify _search . . . . . . . . . .
Modify Index . . . . . . . . . . .
URL Manager . . . . . . . . . . .
Carousel Widget . . . . . . . . . .
CarouselWidget.php . . . . . . . .
carousel.php . . . . . . . . . . . .
Pages Index . . . . . . . . . . . .
Carousel Settings . . . . . . . . .
carousel_settings table . . . . . .
CarouselSettings Model . . . . . .
CarouselSettingsSearch Model . .
CarouselSettingsController . . . .
CarouselSettings _form View . . .
CarouselSettings view.php View .
CarouselSettings index.php View .
CarouselSettings _search View . .
PagesController . . . . . . . . . .
Pages Index View . . . . . . . . .
CarouselWidget . . . . . . . . . .
carousel.php . . . . . . . . . . . .
Main . . . . . . . . . . . . . . . .
Summary . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
547
547
549
550
550
550
553
554
555
563
568
574
574
576
580
584
588
589
591
592
594
597
598
602
606
610
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
612
615
619
620
622
623
628
630
638
638
639
640
643
645
647
648
CONTENTS
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
649
650
650
652
652
654
656
656
658
660
661
661
662
663
663
663
671
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
673
673
675
676
679
681
Features
Some of the features you get with the install of the advanced template include:
If you dont understand something in that list, dont worry, we will be covering it in detail. Just
know that it really is amazing what Yii 2 does for you. But no matter how great a framework is, you
still need to do more to make it support a real application.
So to all the out-of-the-box features, will be adding:
These are all things that your web application is likely to require, regardless of the type of site it is.
So, as you learn Yii 2 with this book, you will be building a template that you can expand on for all
of your future applications.
This book is perfect for beginning PHP programmers who are ready to move on to framework
development. The Yii 2 PHP framework is highly scalable and extensible, and loaded with features.
We introduce you to this wonderful framework and explain in detail everything you need to know
to get up and running. You will love Yii 2!
Advanced Php artisans will be able to zip through this book and get up and running quickly on Yii 2,
a phenomenal php framework. This will not only save them time on projects, but also fully leverage
the benefits of an open-source framework that has an entire community behind it.
The main style of this book, however, is for beginners. Theres a lot of granular detail to help people
who have some experience with PHP but have not really jumped up to advanced object-oriented
programming yet.
We try to make sure we fully understand how the framework works, how it uses OOP to create
an intuitive development layer that allows many different level programmers to achieve the results
they are striving for.
In any case, learning Yii 2 gives you hands on experience with object-oriented programming with
practical results. You end up with a working website.
Upsides
Here are some of the obvious benefits:
Downsides
There are a couple of downsides to using a framework that should be pointed out. First, all the code
that comprises the framework creates server overhead and this can be a real problem. Luckily there
are caching options available which will reduce the effects of this, and for enterprise applications,
you can use raw sql to minimize query time. So dont let the server overhead stop you from using a
framework.
The other thing is that obviously when you are working with the framework, you are working with
a vast amount of code that you didnt write and it takes time to figure out how it works. Some of
the framework code can be quite cryptic depending on your level of skill and experience, so dont
expect to instantly understand everything. Its not going to happen.
Of course you already knew that there was a learning curve, which is why you are reading this
book. And while it takes time to learn someone elses code, which can be a pain, it would be far
more painful to have to write a custom framework from scratch. All things considered, using a
framework for enterprise development is a wise choice.
Ok, so the easy part is to figure out that utilizing a framework will help you develop a more organized
and robust project, but now comes the hard part. You have decide which framework to use.
Anyway, we collectively researched everything we could find on the major PHP frameworks. I
personally read all of the documentation and we had long engineering discussions about what
we thought would work. You cant imagine my frustration with the fact that I read all this
documentation and walked away from it feeling less knowledgeable than before I started reading it.
Our team of programmers did have a preference however. They felt that Yii 1.1.14 was the best
choice. This was the version of Yii that was available at the time we were deciding this. So the team
adopted that framework and never looked back. They loved it.
I, on the other hand, remained frustrated. Since I was only a novice programmer, I really struggled
to learn it. I didnt find it very intuitive. Especially after comparing it to other frameworks, where
they were trying so hard to make everything integrate beautifully, the architecture of Yii just seemed
ugly.
I got so frustrated at one point, that I started looking for another option.
Other Options
I would find some beautifully written documentation for a new framework and run it past the team.
I always got the same response. The team was happy with Yii.
They told me it might be difficult to learn, but it was easy to use, once you knew how it worked.
Because of that, I committed myself to learning it. It was slow going and a rough ride. I wasnt
getting it. I was working through chapter 10 in a book on Yii 1.1.14, thinking I would never really
be able to build an application on my own in less than a hundred years. Too many roads seemed to
go nowhere.
Then a miracle happened.
Yii 2 Arrives
I found out about the Yii 2 alpha. I was curious to see what the differences were in Yii 2, which had
been 3 years in the making at that point. So I jumped in and to my utter and complete surprise, I
instantly connected with it. I understood the structures. I could write code that actually worked!
What a great feeling that was.
I have personally found Yii 2 to be the most intuitive and elegant of all the PHP frameworks that
I have studied. I have so much enthusiasm for it that I want to share it with every programmer I
know, and even those I dont know, so it has motivated me to write this book.
With Yii 2, even as a beginner, I was able to stand up a working website that has a data-driven user
model, with both a frontend and a backend. Right out of the box, I get a working user model, with
forgot password functionality, which is also integrated with Bootstrap for mobile-responsive design,
without having to do any programming whatsoever. How cool is that?
Although I was a beginning programmer when I was studying the PHP frameworks, I did have
experience working with databases and this is one area in my opinion where Yii 2 really shines.
Gii
Yii 2 has a code generation tool called Gii. I pronounce that with a soft g, but I have no idea if that
is the right way to say it or not.
Anyway, Gii analyzes your database tables and automatically builds PHP models from them. Not
only that, but it analyzes the relationships between tables and automatically generates the relational
code into the models. For example, if you had a data structure with 30 tables, and half of them had
a user_id column that was meant to reference id of the user table, Gii would build the appropriate
relationships for you, each time you built a model. Not only is this a time-saver, but this also gives you
very consistent code because it is always done the same way and it helps you adopt this discipline.
Its worth mentioning that other frameworks work exactly the opposite way. With them, you build
the model first, then do a migration to the database to create the table and corresponding columns.
So the big difference is that you are building your data structure piecemeal as you go along, whereas
in Yii 2 you have the option of having a more complete data structure to begin with.
Both approaches work, however they represent drastically different workflows. In my opinion, the
migration/piecemeal approach to data structure only really works for a single developer or a very
small team working on a small project. The reason why I say this is that although democracy is
probably the best system politically, imagine a world where each developer makes up their own
data structure and implements it. How consistent would that be? What if the right hand didnt
know what the left hand was doing? In larger teams, this is a recipe for chaos. This is why enterprise
development teams usually have a database administrator, also known as the DB, and only they can
create or delete data structure.
DB-First Approach
Since Yii 2 allows you to essentially import the models from the data structure, you can start your
project by really thinking through your data structure. Overall I like to avoid talking about too much
theory because the time is better spent working through hands-on examples, but I think its worth
taking a moment to think about what a well thought-out data structure really means.
Whether you are a single developer or part of an enterprise level team, you are essentially being
given the same task, the same overall mission. You have to serve data from a database into a browserfriendly format, typically using PHP, HTML, and Javascript. We use a PHP framework to make this
task easier, and by saying that, we are admitting upfront that its not an easy task. Why is that?
The database is a very reliable and consistent piece of software, which allows us to create a relational
data structure.
MySql
Throughout this book we use Mysql as the database, which, in addition to being free, is capable of
powering enterprise data for web applications.
Because of the structure of the database, with its indexes and primary keys, a database can serve
data very efficiently. In the simplest terms, this means it is very fast. Its also very deep. It can hold
millions of records, which can be retrieved, if structured properly, in milliseconds.
Another key aspect of the database is that it allows us to structure the data in such a way as to
connect things like the users address and their username as if they were one record, but hold them
in separate tables as separate records. The more you can break down the data structure into discrete
components like that, the more powerful it is. This is called normalization of data.
The problem is that the more refined the database is, the more effectively normalized it is, the more
complex it is to deal with in PHP. You end up having to connect a lot of PHP models together to
represent the data correctly.
Now this might be getting too heavy on theory for a beginning book, so we wont take this much
further for now, but the point is to understand the nature of the problem that the framework helps to
solve. The easier it is for you to connect the models via the framework, the more power you derive
from your database.
Improved Workflow
In my opinion, Yii 2 stands alone in how it helps you connect the models to the database, leading to
improved workflow, efficiency, and overall design capabilities. It frees you to build a detail-rich data
structure that will ultimately result in the end user being more engaged. I believe that Yii 2 does this
more efficiently and deeply than any of the other PHP frameworks, that is why Im so committed
to it and so interested in sharing it.
which has 200 videos on PHP. Great for an introduction, but not much more. I followed that up
with a quick read of Richard Reeses book on Java, which helped me understand object-oriented
programming better, since everything in Java involves a class. Also, when I looked at PHP again, it
seemed simpler. I also went through the basics at:
W3 Schools
W3schools.com is a great learning resource. You can play with the code online at that site.
And then of course there is Php.net itself which is where we find all the docs for the language and
sometimes very complicated examples. I learned a lot there and got lost a lot too, thats the way it
goes. Try it, youll see what I mean.
At any rate, to be able to work with Yii 2, you should understand the basics about objects, arrays,
and control structures like foreach loops. You should know the components of a class, properties and
methods, etc. Take a look at:
OOP for Beginners
You should be able to get through that tutorial very easily. If not, go back and study it before trying
to tackle Yii 2. Also, Yii 2 uses PHP 5.4 and above, which supports new array syntax and namespaces,
both of which will be utilized extensively.
If you are light on programming experience, but full of enthusiasm, you should do well, as long as
you are willing to do the work and are patient. At any point, if you dont understand something,
you can stop and take the time to research it on Google or stackoverflow or PHP.net. PHP is a welldocumented and well-supported language, used by countless programmers who will try to help you.
Also, I took a lot of care to label the sub-sections of this book, so you can easily find what you
are looking for, if you need to refer back to it. Many times you will want to return to a section to
reference something and Ive done my best to make that as intuitive as possible.
Any alternative that will let you run those programs is fine, you do not need Xampp to follow this
book. On the other hand, its pretty easy to get up and running with Xampp, one reason why I
use it. The tricky part is setting up environment variables on a Windows machine, but that is well
documented and I have provided download and setup links for your convenience, so you can check
those out if you need to.
Even though everything for Mysql can be done in PhpMyadmin, I also recommend setting up
Mysql workbench. Workbenchs EER (Enhanced Entity Relationship) Diagrams help you see the
relationships and make creating tables and foreign keys a snap. We will use photos of Mysql
Workbench to show you table structure later in the book.
Download Mysql Workbench
You should familiarize yourself with how to create a database, how to sync a diagram model to a
database, and obviously how to create tables and columns. To build a database-driven application,
you need a basic understanding of sql, nothing too deep, but you should know how basic queries
work and the concept of joining tables for queries. And since we use MySql, you need to be familiar
with it. If any of that is new to you, the good news is that you can google up some tutorials and find
everything you need for free. W3 Resources MySql tutorial are a great reference.
For my IDE, I use PhpED. IDE stands for Integrated Development Environment, and helps you
organize projects and code. Most developers use some form of IDE as opposed to just a text editor.
Im recommending Eclipse or Netbeans for this project, however, because both are free whereas
PhpEd is a paid IDE. In order to install Eclipse, you will have to install the Java sdk first.
Download Eclipse
Download Netbeans
You will also need to install Composer, which you should do after installing xampp, which means
after PHP is installed. In order to run Composer, you must first enable curl in your PHP build. You
will also need to set an environment variable for it if you are using windows.
Download composer
Enable Curl
I also recommend using git, which provides version control. Version control is a handy way of
saving your work so you can step backward easily if you need to. When you are dealing with a large
number of files that are constantly being updated, this is a great help. Git also protects you in a team
environment from someone overwriting your work because you can simply step back to a previous
version.
Download Git
Lastly, I recommend console2 for Windows users, which is a command line tool that is a little prettier
than the standard windows prompt. This makes it easier on the eyes and just a little easier to work
with.
Download Console 2
In order to get your development environment working with Yii 2, you will need to add both a vhost
entry into Apache and a local host entry into your hosts file. We will go through each step for that
in detail.
Like I said earlier, if you prefer to use different tools or, for example, a linux machine for development,
that is your choice.
I provided links and reference pages for installation, but for beginners, this may prove to be difficult.
You can use the installation of the development environment as one of the tests to see if you are ready
to tackle Yii 2. Just dont give up easily. If it doesnt go well, you can always get help from a more
experienced programmer.
Tip
Also, and this is a tip for beginners, almost everything you will go through as a programmer
has been gone through by other programmers before you and this is especially true for
configuration errors. Dont be afraid to use Google for help in troubleshooting setup. You
will end up using it more often than not.
Once youve got everything up and running, spend a little time learning your way around the tools.
It will make your efforts developing in Yii 2 go a lot smoother.
Errata
Although I have poured over every line of code in this book at least a hundred times and built the
examples from scratch twice just to make sure I could follow the directions, mistakes are bound to
happen, such is the nature of technical writing. I am actively updating errata as I go, so I do hope to
be able to quickly correct any errata I am made aware of. You can help by emailing me if you find
something, everyone will appreciate it.
Formatting Tip
In certain cases, I had to format my code using two lines where one would be appropriate,
in order to avoid line breaks from the wordwrapping in PDF and other formats. The
wordwrapping in PDF causes special characters to appear, which break the code, so I had
to avoid that the best I could. As a result, Im not recommending you follow the code
examples as an example of style. I would recommend following the PSR-2 Guide, available
here: PSR-2 Coding Style Guide
You can format your code with a formatter at Php Formatter, if you want to make it more readable.
Obviously be careful not to break the code. I will also be supplying Gists for each block of code that
we write in the book, where the block of code exceeds 3 lines. If you dont know what a Gist is,
dont worry, we will cover it in detail later.
10
Summary
I know it can be a little intimidating at first, especially when you realize that Yii 2 is not just some
trivial set of library files that you can master in a few days, but hang in there and be patient. We are
going to tackle it one step at a time.
11
So let me conclude the introduction with the following thought. Learning Yii 2 will come easy for
some people and they are very lucky. If you are in the other camp, the ones that have to work hard
to learn it, I can tell you that I know exactly how you feel. It was hard for me too. But I can also
tell you that you can be optimistic. You can do this. Just stick with it and move at your own pace.
And soon you will be amazed at how you are using Yii 2 to power your applications and you will
be even more amazed at what you can create with it.
See chapter 1 for links to free downloads on the above tools, if you have not already installed them.
If you are not at this stage, you need to go back to the introduction and make sure you have all the
required tools installed.
Hate Windows or Xampp? Not a problem. Obviously, you do not need to follow on Windows to read
this book. If you are working directly on a LAMP stack or something else, you just need to know the
linux commands. I dont provide them here, but you can easily google them. Just to reiterate, these
instructions are xampp on Windows, but there are only minor differences, so you should be able to
figure it out if you are using a different system.
For your convenience, Im also listing a link to the Yii 2 guide for Advanced App installation:
Yii 2 Advanced App Setup
Ok, lets get started:
13
Notepad will open. Select file open and the path to vhosts, in my case:
C:\xampp\apache\conf\extra\
14
Then select:
httpd-vhosts.conf
Please note that c:\var\www is where I store my project folder. If you are putting it in a different
folder, c:\xampp\htdocs for example, you need to use that instead in the above host entries. Also
note that I use backslashes in my vhosts file. Some environments may require forward slashes, so
try that if you have a problem.
Trouble-shooting Tip
Make sure the line:
Include conf/extra/httpd-vhosts.conf
is uncommented in your xampp/apache/conf/httpd file, otherwise the above configuration
will not work.
15
c:\Windows\System32\drivers\etc
yii2build.com
www.yii2build.com
127.0.0.1
backend.yii2build.com
Note that we are running MySql as a service, but not apache. If you did not set up xampp yet,
obviously, you will need to do so before continuing. I recommend that you have all your tools set up
and configured before proceeding and that you take some time to familiarize yourself with them. I
included a xampp video link in chapter 1 that you can refer to as well.
16
If you type yii2build.com and backend.yii2build.com into your browser, they should both return the
phpinfo output, which also conveniently gives you a chance to check to see if you have PHP 5.4 or
greater, which is what you need to run Yii 2.
17
Php Info
If the page does not resolve, go back and check your hosts file and/or your httpd-vhosts.conf. Make
sure to restart Apache after making changes. Make sure you have local host entries for the domain,
yii2build. Refer back to step 2 and 3 if necessary.
At this point, you should be able to see that your host entries are correct and that you are running
the correct version of Php. This is independent of Yii 2 and composer, so successfully implementing
step 5 gives you a test point for the first part of our setup.
If this all checks out, you have successfully tested your host entries and you should delete these test
web folders and their contents. Obviously leave the root folder, yii2build, in place.
Trouble-shooting tip: If you are using windows, you might have trouble deleting a folder. This is
due to permissions of the file being set to read only. You can right click on the folder and use the
properties menu to make adjustments. Use Google for exact details if you need help with that as it
18
composer self-update
If you get an error message, check your installation of composer. If you dont have composer
installed, Google it for instructions on installation into windows and xampp.
You will also need to make sure the following plugin is installed into composer. Issue the following
command from the same directory where you did self-update:
composer global require "fxp/composer-asset-plugin:1.0.0-beta4"
19
Asset Plugin
If the above plugin is not installed, composer will not act correctly. The good news is that as long as
you have composer working, the plugin is easy to install with the one simple command from above.
Please note that in order to access the plugin, you may have to sign in with your Github account
because it may ask you for your username and password. If you do not have a Github account, just
go to Github.com and signup for a free account. It only takes a minute and its free. You will only
have to do this once.
Tip
I checked the Yii 2 Guide and the latest recommended version of the plugin is fxp/composer-asset-plugin:1.0.0-beta4 If for whatever reason, that version of the plugin is out of
date, use Google to find the correct version. You can also try @dev, which should work,
but you never know. I will do my best to keep the book up-to-date, but these are the kinds
of things that will be hard to keep track of. When going through setup in programming
books, these are common problems, so this is just a heads up.
Tip
The directions in the guide are slightly different in that you can set the project folder by
naming it as the last parameter in the install. It can be confusing for beginners though,
which is why Im recommending that you follow these directions, which has an extra step,
but allows you to check to see if the host entries are working before you install Yii 2.
20
Folder
Using windows explorer, open this folder, and you will see all the framework files. Select all files
and copy them one level up to the root yii2build folder, then delete the yii2-app-advanced folder.
So, just to make it perfectly clear, now you should have the root folder, in this case yii2build, with
the framework files inside it on the first level. There should be no yii2-app-advanced folder at this
point. It should look like this after you deleted it:
App Folders
21
It will ask you if you wish to initialize in development or production. Select 0 for development. Then
confirm Yes.
Devel Setup
22
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=yii2build',
'username' => 'root',
'password' => 'yourpassword',
'charset' => 'utf8',
],
Obviously substitute your actual password into the config. Dont forget to save.
Migrate
Confirm yes.
Confirm Yes
This will build the necessary tables in your database. You can check PhpMyadmin and you should
have the following tables in the yii2build:
migrations
user
23
Migrate Success
Commit in Git
You have to stage changes before committing. You may have to unlock index or click continue from
a pop up dialog box. You will also need to enter a comment before committing.
To view the repository from the repository menu, select visualize all branch history. This will show
you the current master branch and its history, great for tracking your changes and stepping back if
you need to. Version control is very important on a project of this size. It is unlikely that you could
24
get through the project without it, so dont skip this step. Remember to save, at minimum, a commit
at the end of every chapter of this book. You should probably do it more often than that.
Please note it is not necessary to save your project in a repository on Github, you can do that if you
like, but we dont cover that in this book. We use GIT for local version control only.
Yii 2 Build
Since no access control is differentiated at this point from frontend to backend, you can log into
backend by going to backend.yii2build.com and logging in. In both cases, login will simply return
the index page and in the nav bar display the user name and the logout link.
Trouble-Shooting
If its not resolving, then check your hosts file and httpd-vhosts.conf. Make sure Apache is running
in xampp and has been restarted after making changes to the host files. Make sure your version of
PHP is 5.4 or higher, that is required for Yii 2.
If you are seeing a directory tree, instead of the homepage, you did not successfully run the init,
go back to step 10. If you can see the homepage, but get a DB error when you try to register, make
25
sure in PhpMyAdmin that the yii2build database exists, that you have the correct password for it,
and that you have entered those settings in yii2build/common/confi/main-local.php. Also make sure
Mysql is running in xampp, see photo in step 4 for reference.
If you still cant get it to work, start over or at least from the point where you confirmed host entries
are working and that you are running PHP 5.4.
Summary
Congratulations, the hardest part of the book is over. Hopefully, this went smoothly for you. If you
did have problems with setup, repeat the steps until you get it right. If you are sure everything is
right, but its still not working, consult with the individual docs of the components to see if something
changed since this book was written. Google is typically very effective for this, when called to serve.
In the next chapters, we will begin working our way into development with Yii 2. We start with a
brief tour of the MVC architecture, but we dont spend a lot of time on theory, unless we can use it
to code. Instead, we dive in quickly in the subsequent chapters.
Ive learned through personal experience that explanations of the broader concepts work better when
they are coupled with practical implementation, which is why I learned almost nothing from most
of my online OOP lessons, just vague impressions of interfaces and class inheritances. Not to worry.
One of the great things about Yii 2 is that it pulls together so many of the principles and concepts
of OOP in such an intuitive way, that you will understand the theories as you go. At least you will
see them demonstrated.
directory Structure
You can see the application is divided between backend, common, console, environments, frontend,
tests, and vendor folders.
MVC Pattern
Yii 2 follows the MVC design pattern, where M stands for Model, V stands for view, and C stands for
controller. Were going to discuss this briefly, but just for an overview. The best way to understand
it is to work with the code and the directory structures directly, which we will do shortly. Here is
another view of the structure with some of the folders open:
27
App Structure
You can see that the backend and frontend folders have folders named models, controllers, and
views. The common folder has models, but no controller or views. You might want to take a few
moments to look in all the folders to see what is there.
In Yii 2, the model is responsible for entering and retrieving data from the database. This includes
any relationships that it needs from connected models, for example, a user and a user profile.
When a web request comes in, the controller typically routes it to the model, where it communicates
with the database, then returns its results for display in the view. This allows for a separation of logic
and presentation. You get fat models full of php, skinny controllers that mostly just do routing, and
views that are light on PHP and deal more with HTML and javascript for presentation.
Thats probably all we need to say about it as abstract theory. It works well and we will see how Yii
2 implements this pattern and how easy it is to understand in practice.
28
Index.php
There are exactly two points that should be accessible from the web in this application. Both backend
and frontend have a folder named web within them and within that folder is file named index.php.
If you recall, we set our hosts entries to look for this file, so that backend.yii2build.com goes to the
backend folder version and yii2build.com goes to the frontend one. Each of these files is identical
and looks like this:
<?php
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
require(__DIR__
require(__DIR__
require(__DIR__
require(__DIR__
.
.
.
.
'/../../vendor/autoload.php');
'/../../vendor/yiisoft/yii2/Yii.php');
'/../../common/config/bootstrap.php');
'/../config/bootstrap.php');
$config = yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/../../common/config/main.php'),
require(__DIR__ . '/../../common/config/main-local.php'),
require(__DIR__ . '/../config/main.php'),
require(__DIR__ . '/../config/main-local.php')
);
$application = new yii\web\Application($config);
$application->run();
The first two lines check to see if the constants exist or define it for debug and dev.
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
Then come the require statements for files necessary to run the app, including the autoloader:
require(__DIR__
require(__DIR__
require(__DIR__
require(__DIR__
.
.
.
.
'/../../vendor/autoload.php');
'/../../vendor/yiisoft/yii2/Yii.php');
'/../../common/config/bootstrap.php');
'/../config/bootstrap.php');
$config is set by the ArrayHelper merge method, which requires the files specified:
29
$config = yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/../../common/config/main.php'),
require(__DIR__ . '/../../common/config/main-local.php'),
require(__DIR__ . '/../config/main.php'),
require(__DIR__ . '/../config/main-local.php')
);
You can see it goes up 2 directories to find the common folder for that config. Then it goes up one
directory to find the configure for frontend or backend, depending on which index.php file is doing
the calling.
Then finally, we create a new instance of the application model, taking the config into the
constructor, so now $application becomes the instance of the application. Then we fire off its run
method:
$application = new yii\web\Application($config);
$application->run();
Routing
So lets get back to index.php, the file acts as a doorway to the application, creating the instance of it.
When we are typing in a url for our application, we will always be calling index.php. Yii 2 handles
all the routing for us, so when we want to get to the site home page for example, the route looks like
this:
yii2build.com/index.php?r=site/index
Thats not very pretty. You can set the urls to be pretty in the config, which helps their search
engine friendliness and you can also eliminate the need to show index.php in the url, but we wont
be covering that until chapter 13. By waiting on that, we eliminate having to debug the url or apache,
if a problem with the page should arise.
Ok, lets get back to routing:
30
The r=site/index tells Yii 2 that we want the site controller and the index action. If an incoming
request does not specify a route, which happens when someone just types in yii2build.com for
example, then, the route specified by yii\webApplication::$defaultRoute will be used. The default
is set to site/index, which, as we mentioned above, specifies the site controller and the index action.
If no action is specified, the controller assumes you want the index action. Example:
yii2build.com/index.php?r=site
This returns the index action of the site controller. In most cases, the action will render an associated
view, a view with the same name as the controller action. Common actions and views are index,
view, create, update, delete.
We often refer to the create, read, update, and delete actions as CRUD.
Using Gii
We will be using Yii 2s built-in rock star module, Gii, the all-time greatest code generation tool ever
built, to help us make a lot of CRUD. And when we use Gii to create CRUD, we are often creating
the controller at the same time, so we can generically expect the CRUD to include the controller.
Dont worry if this is a little unclear now, it will make a lot more sense later when we are creating
our files. And yes, I worship Gii, and Im pretty sure by the time were done, you will too.
If you look in the views folder under frontend, you can see a folder named site, which has an
index.php file in it. This is the view page rendered by the index action of the site controller. The
site controller itself is located at frontend/controllers/SiteController.php
Browse around the folders. Inside of backend, you will find controllers, models, views. You will find
the same in the frontend folder. In the common folder, you see config, mail, and models. Overall,
you can see the consistency in the naming conventions and they make Yii 2 easy to understand from
an MVC point of view.
So obviously, this is quite different from simple web applications where you would have a url like
samplesite.com/about.php. If you are tempted to skip learning Yii 2 because your current application
requirements do not need to be so robust, keep in mind that over time, applications requirements
tend to grow.
If today your client doesnt need a form with robust validation rules, it doesnt mean that he wont
need it tomorrow.
Bootstrap
Also, with Yii 2, you get the frontend framework Bootstrap integrated out-of-the-box. If you are
unfamiliar with Twitters Bootstrap framework, I recommend you check it out, it has fast become
the industry standard. You can check it out here:
31
Get Bootstrap
You dont need to download or do anything though, because like I said before, Yii 2 comes with
it already integrated as a default. That means you get a platform-responsive css that scales to the
device, allowing you to create mobile-first design from the start. And that, my friends, is just the
cherry on top of the cake!
One day your client wants a nothing website and the next day he wants mobile css. You can deliver
because you are already there. Anyway, Im not trying to sound like a salesman. I truly love this
platform and it shows.
In our previous projects, we might have created header and footer files that we could require in our
individual pages, simple but inefficient. What if you forgot to include the file or made a typo to a
previous version? What about theming and other advanced approaches?
Yii 2 has a cool solution for this by using layouts. Views are injected into the layout and there are
methods available at the site config level or the controller level to specify which layout to use. A
default layout is already there, so you dont need to do anything if you dont want to change it. You
can also use nested layouts, if you feel that is necessary.
For our purposes, we are going to stick with the default layout, which is located at frontend/views/layouts/main.php. The only thing we are going to note at this time is that this tag in
the middle of the page:
<?= $content ?>
This is where the view page gets injected. So now you know the header is above $content and the
footer is below it. Dont worry, we will be making changes to this file and will be coming back to it
later in the project.
Debugger
One other thing we should mention is the rather conspicuous Yii Debugger tool that sits at the
bottom of the page, when you view the advanced template in the browser. This has many useful
utilities, such as
Configuration
Logs
Profiling
Database
Asset Bundles
Mail
We dont spend time on it in this book, but in your programming workflow, this is incredibly handy.
You can check to see which queries are being executed, how long they took and many other helpful
32
details about how your application is working. Take some time to familiarize yourself with it. Youll
get it just by playing with it. If you are not using it, you can click the arrow at the bottom right of
the browser and hide it.
Summary
As we said in the introduction, Yii 2 is not a trivial implementation of the MVC pattern. Its an
extensive framework that is robust and easy to use, once you are familiar with it. The learning
curve for beginners can be steep, but stick with it, its worth it.
Im going to do everything I can to help you along, method by method. It will be a little fuzzy at
first, but as we go along, and we get deeper into the project, it will begin to make sense, and the
pieces will start to fit.
So, what shall we build as our sample application? What would demonstrate useful features that
many projects would share? And can we actually use anything we build here in a real project?
Asking myself those questions led me to conclude that this book should build an application template
named Yii 2 Build. Now wait a minute, isnt the advanced application installation itself a template?
Yes it is. But we are going to take things a little further.
We are going to create a basic RBAC system with UI that allows us to set roles, statuses, and user
types to control access to both the frontend and backend of the application. We are going to build
an upgrade controller, so that if we want a paid area of our application, we can enforce that rule.
We are also going to create a user profile model that can be extended or modified to suit your needs,
but one that shows us how to control access to views that should be private to the user who owns
them.
Our goal will be to create a working application that you can use as a model for future projects, one
that is much further along in development than the advanced application template. As cool as the
out-of-the-box template is, we can do a lot more, and learn the ins and outs of the framework along
the way. Lets get to it and lets have some fun!
Also note, at the end of chapters, you will see:
33
Commit To Git
This is a reminder to commit your changes to version control. No need to do it now, since we didnt
change anything. But stay on top of that, it will be a big help to you if you need to step backwards
for any reason.
35
powerful approach to authorization. It will take us through multiple model setups and step us into
relationships. Most of the code is really, really simple.
Youll also see how easy it is to create all this with Gii, Yiis built-in code generator. Youve probably
heard about Gii and it is an amazing tool, but were not ready to use that just yet, we will come back
to it next chapter.
Instead, we just need to start with some modifications to what we already have. The user table and
User model were created for us automatically by the advanced application when we installed it, and
while its close to what we need, we have to make some important changes.
36
Note the use of unsigned for the id column. This means id cant be a negative number. Also make
sure the created_at and updated_at fields are of the type DATETIME. For some reason the initial
build is set to save as int for those fields, but we are going to work with behaviors on the User model
to make sure they are saved correctly as DATETIME.
Note: If you have followed earlier instructions and added a test user, you should probably delete that
record before syncing and changing the datastructure in the DB because it might not overwrite the
existing record correctly.
Dont forget to sync to the database:
Synchronize to Database
Another note: If you are more comfortable using Php MyAdmin to make these changes, it is perfectly
fine doing it that way, as long as you follow the data structure given.
Once we make the change, dont bother testing the site, nothing is going to work. We are going to
have to change the User model before we test the site again.
37
Tip
Before we start, a quick tip in case you didnt notice. Only view files have closing ?> tags.
Do not include closing ?> tags in your models and controllers.
<?php
namespace common\models;
use
use
use
use
use
use
use
Yii;
yii\base\NotSupportedException;
yii\behaviors\TimestampBehavior;
yii\db\ActiveRecord;
yii\db\Expression;
yii\web\IdentityInterface;
yii\helpers\Security;
/**
* User model
*
* @property integer $id
* @property string $username
* @property string $password_hash
* @property string $password_reset_token
* @property string $email
* @property string $auth_key
* @property integer $role_id
* @property integer $status_id
* @property integer $user_type_id
* @property integer $created_at
* @property integer $updated_at
* @property string $password write-only password
*/
38
/**
* behaviors
*/
/**
* validation rules
*/
['username',
['username',
['username',
['username',
39
['email',
['email',
['email',
['email',
];
}
/**
* @findIdentity
*/
/**
* @inheritdoc
*/
public static function findIdentityByAccessToken($token, $type = null)
{
throw new NotSupportedException
('"findIdentityByAccessToken" is not implemented.');
40
/**
* Finds user by username
* broken into 2 lines to avoid wordwrapping * @param string $username
* @return static|null
*/
/**
* Finds user by password reset token
*
* @param string $token password reset token
* @return static|null
*/
public static function findByPasswordResetToken($token)
{
if (!static::isPasswordResetTokenValid($token)) {
return null;
}
return static::findOne([
'password_reset_token' => $token,
'status_id' => self::STATUS_ACTIVE,
]);
}
/**
* Finds out if password reset token is valid
*
41
/**
* @getId
*/
/**
* @getAuthKey
*/
/**
* @validateAuthKey
*/
42
/**
* Validates password
*
* @param string $password password to validate
* @return boolean if password provided is valid for current user
*/
/**
* Generates password hash from password and sets it to the model
*
* @param string $password
*/
/**
* Generates "remember me" authentication key
*/
43
/**
* Generates new password reset token
* broken into 2 lines to avoid wordwrapping
*/
/**
* Removes password reset token
*/
44
45
Tip
If you are using the code from the book and not the Gist, the code for User.php is not
formatted exactly like you would want it in your file. There are a few instances of two
lines being used when there should be one. The reason is that PDF and other formats break
the line with a wordwrap and insert special characters that mess up the code, so I have to
proactively format the code so the line doesnt break. It doesnt always look pretty, but at
least the code will function. You should find these instances and convert them to a single
line by removing the white space. The public static function findByUsername($username)
and public function generatePasswordResetToken() have the extra line in the body of the
function.
Role
Status
UserType
Profile
Gender
In the next chapter, we are going to create the database tables for these models, and then the actual
models themselves.
A lot of programmers will create database structure one table at a time and feel their way forward.
Typically they use migrations to accomplish this. Other than the initial migration that created the
original user model, we are not using migrations.
46
Im a big believer in thinking through the data structure and creating it all at once, as opposed to an
adhoc approach. Thats not to say you cant refine and change as you go, but a little forethought goes
a long way. You are of course free to use migrations if you wish, especially if you are comfortable
using them. See the Yii 2 Guide for details:
Yii 2 Migrations
Constants
One thing that might pop out at you from that list of new models, especially with Role, Status, and
UserType, is that these data structures could alternatively be handled by constants. While that would
be probably easier to implement, I favor putting things like status values in the DB. The reason for
this is that I can then create an Admin UI that allows me to update and create new values, without
having to go into the code.
Take Role for example. Lets say that you have a role called admin, which grants access to the
backend. It has a value of 20. You set up your constant as follows:
const ROLE_ADMIN_VALUE = 20;
But then you decide that you need an even more expansive role, lets call it SuperUser. You would
have to go back to the code, find every instance where you are using the constant, create another
constant, and add it to all the supporting methods that will populate the names of the Roles for
dropdown lists, etc. Its easy enough to do, but in my opinion, not the best way.
I would rather have UI in the backend that allows me to simply add a DB record that defines the
new role and gives it a value. Then, if I have coded my methods correctly, I have it available to me
everywhere. As we progress in this book, you will see how this plays out.
Now if you check under our class declaration, you will see we left one constant in place:
const STATUS_ACTIVE = 1;
I kept the constant there for a good reason, even though it violates DRY (as far as for what we
are going to build), because the status value active is vital to the registration and recover password
system and we dont have our supporting tables and models built yet. I leave the constant in place,
so that we can get the site up and running. The site needs this value to work and its one of those
cases where Im willing to duplicate for ease of use. You can replace this later with a method if you
choose to do so. Its a trivial matter in later stages to make the change if you wish.
Identity Interface
Going back to the class declaration for a moment:
47
This is Yii 2 class structure that I didnt write, but its not a problem, we can still note a few things
about the model.
In this case, we extend ActiveRecord and implement IdentityInterface, which means we have to
create the interfaces methods in our User class. Well look at the class and the comments written
by the Yii 2 developers provide details on what the methods should do. It will give you some idea of
how it works, but dont worry if you dont instantly know how to write the methods, and you will
see why that is in a moment.
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\web;
interface IdentityInterface
{
/**
* Finds an identity by the given ID.
* @param string|integer $id the ID to be looked for
* @return IdentityInterface the identity object that
*matches the given ID.
* Null should be returned if such an identity cannot be found
* or the identity is not in an active state
*(disabled, deleted, etc.)
*/
on the implementation.
* For example, [[\yii\filters\auth\HttpBearerAuth]] will set this parameter to \
be `yii\filters\auth\HttpBearerAuth`.
* @return IdentityInterface the identity object that matches the given token.
* Null should be returned if such an identity cannot be found
* or the identity is not in an active state (disabled, deleted, etc.)
*/
/**
* Returns a key that can be used to check the validity of a given identity ID.
*
* The key should be unique for each individual user, and should be persistent
* so that it can be used to check the validity of the user identity.
*
* The space of such keys should be big enough to defeat potential identity atta\
cks.
*
* This is required if [[User::enableAutoLogin]] is enabled.
* @return string a key that is used to check the validity of a given identity I\
D.
* @see validateAuthKey()
*/
/**
* Validates the given auth key.
*
48
49
An Interface is like a contract with the subclass. It says if you wish to use my interface, you must
have the following methods. If you dont include them all, it will return an error. Programmers use
Interfaces to control the architecture.
So this IdentityInterface is the contract our User model needs to implement. Ok, so I said we didnt
have to worry about the interface, why is that? Fortunately for us, the advanced template already
implements it for us, and you can find these methods already on our User model, so you dont need
to write a single line of code. Thank you Yii 2 Advanced Template!
The basic template does not come with this implementation, so this is one reason why the advanced
template is actually easier to implement than the basic template. Its one of the primary reasons we
chose the advanced template for this book.
We have made a small change to a number of the interface methods that were provided by the
template, changing the attribute status to status_id to reflect the changes we made in our data
structure. I will point these changes out as we move through each method of the user model.
So lets get back to our User model proper. As we move through the methods, I will also point out
what we included in the use statements to support the method when that is necessary.
The first method we see is:
public static function tableName()
{
return 'user';
}
Hopefully this one is rather self-evident. I wish they were all this easy!
Behaviors
The next method is:
50
I find the concept of behaviors in Yii 2 very intuitive, written with clear syntax and beautiful code.
The method tells the model how to behave, given certain events.
The first element in the array, timestamp identifies the behavior, and we tell it what class we
want to use. Then we define the events that will affect the attributes, in this case ActiveRecord::EVENT_BEFORE_INSERT and ActiveRecord::EVENT_BEFORE_UPDATE. These point to the
attributes, created_at and updated_at, which are also represented as fields in the user table.
Note that we are also defining the value and it will use:
'value' => new Expression('NOW()'),
It hands the string NOW() to Mysql, which is a Mysql syntax for the current DateTime, so the
expression class formats it for us. Without that, it would insert an integer, which is not the behavior
we want. So this method will fire off whenever a record is created or updated and put the appropriate
entry into the database in the correct DateTime format.
The concept of behaviors is used extensively on Controllers and we will be looking at that later. Also
note that in order to use Expression, we have to include the appropriate use statement:
use yii\db\Expression;
Rules
51
['username',
['username',
['username',
['username',
['email',
['email',
['email',
['email',
];
}
This is how easy Yii 2 makes it to enforce validation rules on the model. Its an array format, where
the first value is the attribute, the second is the validator being called, and then come parameters or
conditions. You can check the guide for a more complete list of validators and how to use them:
Yii 2 guide on Rules
The first 3 rules deal with setting defaults. I put white space in between groups for cosmetic reasons
to make it easier to work on rules for a particular attribute, but the order they are in doesnt really
matter.
If we look at the last set of rules, the ones for email, we see that make sure we trim spaces out, email
is required, email is of email type, and email is unique. Yii 2 does all of this for you with this simple
syntax, how awesome is that?
Identity Methods
The next method on the User model is findIdentity, which is an implementation of IdentityInterface,
which we covered previously.
52
This is one of the places where we changed status to status_id. This is followed by:
public static function findIdentityByAccessToken($token, $type = null)
{
throw new NotSupportedException
('"findIdentityByAccessToken" is not implemented.');
}
Here again we have used the status_id attribute instead of status. Another method from the
IdentityInterface, with the same change to status_id attribute:
And the last two methods from the Interface provided by the Advanced Template:
53
54
Since the advanced app template provides the Interface methods for us, we will not cover them in
greater detail. If you wish to read more on them, you can check out:
Yii 2 Security Authentication
Boilerplate Methods
The next few classes are all part of the boilerplate, which we did not change. Again, the comments
provide an explanation better than I can, since these are deep framework methods that I didnt write:
/**
* Validates password
*
* @param string $password password to validate
* @return boolean if password provided is valid for current user
*/
/**
* Generates password hash from password and sets it to the model
*
* @param string $password
*/
55
/**
* Generates "remember me" authentication key
*/
/**
* Generates new password reset token
* line break to avoid wordwrap
* body should be single line in your IDE
*/
/**
* Removes password reset token
*/
If all went well with updating the user table and copying the new User model, you should be able
to use the application again to register a user. If for some reason it doesnt work, retrace your steps
56
and check your spelling carefully. Make sure the DB is updated with the correct fields.
Note: Since we changed our field to status_id, the out-of-the-box forgot password functionality is
now broken. Dont worry, we will fix it later.
SignupForm Model
Lets take a look at the SingupForm model, located in frontend/models/SignupForm.php. You see
there are only 3 attributes:
class SignupForm extends Model
{
public $username;
public $email;
public $password;
The reason why there are no attributes or rules for role_id, status_id, user_type_id, for example, is
that we are setting those by default in the background, not from the form, so they are not needed.
Remember, we set the default value of role_id to 1, and it automatically gets recorded that way when
a user record is created.
Often, user data will be handed to a form model to enforce validation rules or other methods. The
data comes in from a controller, which gives the model the post data from a view that is typically a
form. This sounds more complicated than it actually is.
Its important to know how a user is created in your application, so lets see how this works by
looking at the actionSignup() method on frontend/controllers/SiteController:
57
You can see the method calls an instance of the SignupForm model. The main method of SingupForm
is signup(), which creates an instance of the User model if the form has passed validation:
public function signup()
{
if ($this->validate()) {
$user = new User();
$user->username = $this->username;
$user->email = $this->email;
$user->setPassword($this->password);
$user->generateAuthKey();
$user->save();
return $user;
}
return null;
}
It will try to validate, and if it can validate, it calls an instance of the user class, so it can set the
user properties to what was handed in via form, create the hashed password, generate the auth key,
save and return $user. Its important to note that a return statement, when executed, terminates the
function, so you dont need an else statement here. If there is a $user, it gets returned and the code
never executes return null. If the if statement evaluates false, it will return null. It will be false if
validation fails or if there were some other problem.
Ok, back to the action on the SiteController, where we get a nice nested if statement, which we can
break apart to understand:
58
if ($model->load(Yii::$app->request->post())) {
If the model (SignupForm) can load the post data from Yii::$app->request->post(), which only
happens if there is post data. The syntax for getting the post data is clear and concise:
Yii::$app->request->post()
This brings all the form attributes along as long as the form and form model are built correctly. The
post data can only come from someone filling out the signup form on the view and being passed
along by the action of the view. If that happens, then continue. In this case the view is signup.php
under frontend/views/site/signup.php. We wont go into detail on the form now, but you can check
it out for yourself if you want to.
Next if:
if ($user = $model->signup()) {
Call the signup method of SignupForm. The first thing the signup method does is validate, so if we
dont get past the rules, it will not sign up the user and it will return an error message to the user,
based on rule behavior. If all is well and we get an instance of $user, it continues:
Then the third if:
if (Yii::$app->getUser()->login($user)) {
We are accessing getuser and login user from an application instance of Yii, which has access to
those methods. We talked about creating the application instance from Index.php in chapter 3, so
here it is being used to called a couple of chained methods.
tip
Note, for us to be able to use Yii::$app, we need to have the use statement, use Yii; at the
top of the file.
This simply takes you back to the Site Index.php view, but in a logged in state, otherwise, you get
the signup form itself:
return $this->render('signup', [
'model' => $model,
]);
And with all the validation and internal methods of Yii 2, if you tried to signup and something was
wrong, it will display the error messages as well.
The login method from SiteController is similar:
59
First it tests to see if you are logged in or not by calling the isGuest method. We are using the ! in
front, so if not a guest, you are already logged in and you go to the home page.
Then it uses a different model, the LoginForm model and either logs you in and takes you back to
the page you were on previously, but in a logged in state, or it shows you the login form, again with
errors if you tried to login in and did it incorrectly.
Ok, so we took a quick detour from the User model to give you an idea of how users are created and
to give you a look at the models moving user data through the site. We didnt really go into too much
depth on the controller, we will cover controllers more in detail later, this was more about the models
that are controlling the user. Here we had 3 distinct models, User, SignupForm, and LoginForm that
controlled the users data.
60
Summary
Ok, that was a lot to absorb. If this is all new to you and you are struggling with it a bit, dont worry,
it will become more clear over time as you get used to seeing the same types of methods used to
move data around the site. We will see all this in detail again.
So we are building a reusable template and starting by modifying the User model, which has a lot
of methods on it that reach deep into the framework.
The User model is always drastically different than other models because of things like the set
password method and the other methods that are unique to users. We also touched on the fact that
controllers can sometimes use other models to create and change user records.
The other models we are going to build, such as Role, Status, UserType, etc., tend to be more
straightforward and easier to understand, not to mention, a lot shorter in size.
In the next section, we will use Gii, Yii 2s code generation tool, and you will see how amazing this
really is and how much faster the workflow is.
Role
Status
Gender
UserType
Profile
Note that in the list above, since we are talking about models, we use uppercase, and you can see
on UserType, that I used the format that Gii will create from the convention where the table name
is user_type. We will understand that better later in the book when we create the UserType CRUD.
Creating Tables
Now its time for us to create the rest of the tables. Im going to provide screenshots from Mysql
workbench, which will give us an easy reference for not only what fields we need, but also the
constraints and data types.
Tip
MySQL CONSTRAINTS are used to define rules to allow or restrict what values can be
stored in columns.
MySQL CONSTRAINTS enforce the integrity of database.
MySQL CONSTRAINTS are declared at the time of creating a table.
62
UNIQUE
PRIMARY KEY
FOREIGN KEY
CHECK
DEFAULT
Role Table
Here is the table for role:
role table
Notice that we have used lower case to name the table. If a table name requires two words, we will
separate them with an underscore. We will also use underscore to separate words in column names
as you can see above.
The role table is very simple. Pk stands for primary key, NN means Not Null, and AI is autoincrement. We auto-increment the record ids. We use varchar for role_name and integer for
role_value. You can probably use small int for role_value, I will leave that choice up to you.
Sometimes when you building even trivial data structure, you will want created_at and updated_at,
plus created_by and updated_by, just to keep track of who is doing what and when. But since this
is only holding the names and values of roles, we dont need those fields.
63
Status Table
Ok, lets move on and now do one for status:
Status Table
This is identical to role, only its for status. On both tables we have created so far, we are selecting
PK for primary key on the first column, which is id. We also set it to NN, which is not null, meaning
it is not allowed to be null.
Thats the same type of data structure as the first two tables we created, only we have a table name
with an underscore in it. Gii creates a specific naming convention to handle this, which we will see
later when we create the model, controller, and views.
64
Gender Table
Here we have gender:
Gender Table
Profile Table
And lastly, the profile table:
Profile Table
Our plan is to allow each user to create a single profile, so these will have a one to one relationship
with the User model. So lets add the following method to the User model at the bottom of the class.
Gist:
Get Profile
From book:
65
Make sure you updated the User model with the above. We are jumping around a bit, but that cant
be avoided.
You can see in this case the id of the user is set to the user_id on the profile record. And this establishes
the link between the two models.
Well do that for our other models in a few minutes, after we have set up the new models. Then
we can update the User model with all the other relationship methods it needs to talk to the other
models.
Ok, back to the profile table.
Note that on the profile int columns, I checked off UN, which stands for unsigned and does not allow
negative numbers.
You can also see there is a red diamond on the gender_id column and this represents a foreign key.
Foreign keys are set to tie 2 tables together and Gii can read this data and setup the relationship for
you when it creates the model. We will see this in action later.
If you are unfamiliar with foreign keys in MySql and MySql Workbench, you should take some time
to Google it and learn it, they are an important part of database structure. You dont necessarily
need to use them to follow this book, but you do need to make sure that all relationships are defined
in cases where Gii would have automatically created the relationship for you because of a foreign
key. Its not a problem, just pay careful attention to the needed relationships when we cover them
later.
Right now all you need to know, is that the foreign key for gender_id on the profile table is mapped
to id on the gender table.
Note: If you are having trouble setting the foreign key, make sure the data types match exactly. Refer
to the screenshots above for reference.
Synchronize
Dont forget to synchronize the model with the actual DB:
66
Synchronize
PhpMyAdmin
And thats it. All in all, its a very simple data structure and were going to have a lot of fun with it.
Were going to use Gii to create models, controllers, and views, lots of code that it will generate for
us.
Configuring Gii
Of course we need to make sure we have Gii installed. Go to the following url in your browser:
yii2build.com/index.php?r=gii
If that does not resolve, then you need to check your Composer.Json file to see if you have the Gii
module required. composer.json is in your root directory and should be visible in your IDE.
Again with larger blocks of code, for your convenience:
Gist:
composer.json
This is what my file looks like:
{
"name": "yiisoft/yii2-app-advanced",
"description": "Yii 2 Advanced Application Template",
"keywords": ["yii2", "framework", "advanced", "application template"],
"homepage": "http://www.yiiframework.com/",
"type": "project",
"license": "BSD-3-Clause",
"support": {
"issues": "https://github.com/yiisoft/yii2/issues?state=open",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
"source": "https://github.com/yiisoft/yii2"
},
"minimum-stability": "stable",
"require": {
"php": ">=5.4.0",
"yiisoft/yii2": "*",
"yiisoft/yii2-bootstrap": "*",
"yiisoft/yii2-swiftmailer": "*",
"kartik-v/yii2-social": "dev-master",
"fortawesome/font-awesome": "4.2.0"
},
"require-dev": {
"yiisoft/yii2-codeception": "*",
"yiisoft/yii2-debug": "*",
"yiisoft/yii2-gii": "*",
"yiisoft/yii2-faker": "*",
"yiisoft/yii2-jui": "*"
},
"config": {
"process-timeout": 1800
},
"extra": {
"asset-installer-paths": {
"npm-asset-library": "vendor/npm",
"bower-asset-library": "vendor/bower"
}
}
67
68
You can see under require-dev, I have the line for gii. I have a few extensions included for use
later, including Karitk social, font-awesome and others. It makes sense to just copy this version of
composer.json into your file, so go ahead and do that, then run composer update from the command
line:
Composer Update
69
70
Pay careful attention to the Namespace field. You can see we have the namespace backend\models,
so the Role.php file will reside in that folder and the namespace will be attached to the file. Then
whenever we want to use it, we just include a use statement:
use backend\models\Role;
Click the Preview button. It will auto-generate the file. You can review it by clicking on the file name.
To actually generate the code, click on the green Generate button. Now go check backend/models
and you will see Role.php, perfectly formatted for us:
<?php
namespace backend\models;
/**
* This is the model class for table "role".
*
* @property integer $id
* @property string $role_name
* @property integer $role_value
*/
class Role extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
/**
* @inheritdoc
*/
71
72
/**
* @inheritdoc
*/
So there it is in backend/models. It looks very familiar at this point. Ive already explained in detail
what the rules do on the User model, and rules function the same way here. This is such a simple
model, that we dont have much to talk about.
The attributeLabels method just sets the attributes to label names that will be visible on the
application.
Lets just take a moment to appreciate how clean and simple all that really is. You can see how, once
you get into this flow, things can move quickly.
Ok, moving on.
73
You can see we dont need to set the id field, that is auto-increment. So we are creating 2 records, 1
named User, 1 named Admin. User has a role_value of 10 and Admin has a role_value of 20. Once
the records are added, you should see this on the browse tab for the role table:
If you recall, we set role_id on the user table to default to 1 when a user record is created. So it makes
sense that role_id will map to id, which means a new user gets a role of user when they are created
. Now we just need a few methods on both models to tie this all together.
74
The use statement gives us visibility on the User model. The getUsers method is a standard way of
establishing the relationship from role to users. In this case, we are creating a hasMany relationship
because a single role can have many users. That is also why the method is called getUsers instead
of the singular getUser. In the array, you see:
['role_id' => 'id']
So the first field is that of the relationship and the field it points to in the array is from the model
you are currently working on. The syntax is very intuitive, but its worth spelling this out exactly:
hasMany(User::className(), ['role_id' => 'id'])
className is a method of the related model. The related models field name comes first, followed
by the current models field name, so now role_id on the user table is mapped to role_value in the
role table. Thats it!
Now lets add the following methods to the bottom of the class:
Gist:
User Role Relationships
From book:
/**
* get role relationship
*
*/
75
/**
* get role name
*
*/
/**
* get list of roles for dropdown
*/
Ok, lets look at these one by one. The first one is the getRole relationship, which is the other side
of the getUsers relationship that we added to Role. In this case, Users only have one role, so it is a
hasOne relationship and the method has the singular getRole name to it. Otherwise, the format is
exactly the same as it was for the Role model. It simply says that id on the role table maps to role_id
on the user table. This should be easy to understand.
The second method here for User is getRoleName. This allows us to return the name of the role,
which we will want to do for our backend UI. We put in a ternary test to see if a role has been
assigned, and if so, return the name or the string - no role -.
Finally, we want to return a list of role ids and names to use in dropdown lists in the UI. So we create
the getRoleList method as follows:
76
Here we create the local variable $droptions and assign an instance of Role, with all records
returned as an array. Note how intuitive yii 2s syntax is, it reads just like a sentence. Then we
use ArrayHelper::map method to list the role values and names. In order to use ArrayHelper, we
need to include a use statement for it at the top of the file in the use statement block:
use yii\helpers\ArrayHelper;
This is a very common format for us to have these 3 relationship methods and we will do similar
methods for the other models. Also note, the Yii 2 guide does a pretty good job of listing the
relationship types, so refer to that as well when you are building your applications:
Yii DB Guide
Also, now that we have our getRoleList method, we can use it to enforce a validation rule on the
User model. We only want the model to accept role_id values that are in the range of the values in
the role_value field in the role table. We can get those using the following:
array_keys($this->getRoleList())
If you recall, in getRoleList, the keys are the id records. So this statement returns 1, 2 based on what
we have in our DB so far. If we add new values, they would automatically get added to the list.
So to make a rule out of it, we do the following:
[['role_id'],'in', 'range'=>array_keys($this->getRoleList())],
The above syntax is again very intuitive. Just pop this into your rules method on the User model
and you now have a range of values enforced for entries on the role_id column in your DB. This is
a very common technique and we will do the same exact thing for two other attributes on the User
model, which we will see shortly.
<?php
namespace backend\models;
/**
* This is the model class for table "status".
*
* @property integer $id
* @property string $status_name
* @property integer $status_value
*/
/**
* @inheritdoc
*/
/**
* @inheritdoc
*/
77
78
/**
* @inheritdoc
*/
And lets create the relationship to user. First add the use statement to Status.php:
use common\models\User;
I wont do a lot of explaining here, this is just like the Role model. And just as we did for Role, we
need to add relationship methods for Status to the User model.
Gist:
User Status Relations
From book:
/**
* get status relation
*
*/
/**
* get status name
*
*/
/**
* get list of statuses for dropdown
*/
And now lets add the validation rule for range, since we have access to getStatusList:
[['status_id'],'in', 'range'=>array_keys($this->getStatusList())],
79
80
The order that put this in will not matter, it will observe the rule, but for readability of code, its
recommended that you put this under the other rule for status like so:
['status_id', 'default', 'value' => self::STATUS_ACTIVE],
[['status_id'],'in', 'range'=>array_keys($this->getStatusList())],
When its done, you should see the following under the browse tab on the status table:
81
Status Records
/**
* This is the model class for table "user_type".
*
* @property string $id
* @property string $user_type_name
* @property integer $user_type_value
*/
/**
* @inheritdoc
*/
/**
* @inheritdoc
*/
/**
* @inheritdoc
*/
82
83
This is another simple model along the lines of the other two we have created. So lets go ahead and
add the use statement:
use common\models\User;
Now lets add the getUsers method to setup the relation to users.
Gist:
Usertype Get Users
From book:
public function getUsers()
{
return $this->hasMany(User::className(), ['user_type_id' => 'id']);
}
'user_type_id']);
84
/**
* get user type name
*
*/
public function getUserTypeName()
{
return $this->userType ? $this->userType->user_type_name : '- no user type -';
}
/**
* get list of user types for dropdown
*/
/**
* get user type id
*
*/
public function getUserTypeId()
{
return $this->userType ? $this->userType->id : 'none';
}
Ok, we did 4 methods instead of 3. The first three should look really familiar, since we did the same
types on the other models, and we can quickly add the validation rule for range on user_type_id:
[['user_type_id'],'in', 'range'=>array_keys($this->getUserTypeList())],
One thing not so obvious is the naming convention of getUserTypeName. The name of the method
seems logical, since we always start with a lowercase g for get and then capitalize the remaining
words. Where it gets tricky is in:
return $this->userType ? $this->userType->user_type_name : '- no user type -';
85
$this->userType is using a magic get method, where get is implied, and in this case, Yii 2 doesnt
want you to start with an uppercase letter, the naming convention changes for this special case. This
can be confusing so you have to be very careful to follow naming conventions exactly or things will
break.
The fourth method getUserTypeId returns the id record of the UserType, which we will need for
future use, so not much to discuss about that now.
If youve been a little unclear as to what we intended for the UserType model, these names should
give you a pretty good idea. This data structure is going to come in very handy when we want to
restrict parts of the application to Paid users only.
frontend\models
If all went well, you should see a Gender.php in frontend/models that looks like this:
<?php
namespace frontend\models;
use Yii;
/**
* This is the model class for table "gender".
*
* @property integer $id
* @property string $gender_name
*
* @property Profile[] $profiles
*/
/**
* @inheritdoc
*/
/**
* @inheritdoc
*/
86
87
return [
[['gender_name'], 'required'],
[['gender_name'], 'string', 'max' => 45]
];
}
/**
* @inheritdoc
*/
/**
* @return \yii\db\ActiveQuery
*/
You can see that because of the foreign key we added, Gii was smart enough to add the relationship
for us. How cool is that?
88
1 = male 2 = female
It should look like this:
Gender Records
We dont need to update User because of this model, the User model doesnt call it. Instead, its
tightly related to Profile.
So far, most of the models weve focused are models that contain data structure that every user who
registers with the application must have. The role_id, status_id, and user_type_id will are all set by
default when a user registers, and we have connected them to models that have a data structure that
provides depth to the user.
id
user_id
first_name
last_name
birthdate
gender_id
created_at
updated_at
You can get creative with this and add other attributes, we just settled on these for demonstration
purposes. Just remember if you do add more to it, to add it both in the table structure and in the
model.
Ok, we already decided Profile was going in the frontend, so make sure the namespace field is set:
frontend\models
89
Now we will click through the preview and generate steps and if all has gone well, we have the
following Profile.php in frontend/models. If all went well, you can see the file:
/**
* This is the model class for table "profile".
*
* @property string $id
* @property string $user_id
* @property string $first_name
* @property string $last_name
* @property string $birthdate
* @property string $gender_id
* @property string $created_at
* @property string $updated_at
*
* @property Gender $gender
*/
class Profile extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
/**
* @inheritdoc
*/
90
/**
* @inheritdoc
*/
/**
* @return \yii\db\ActiveQuery
*/
91
92
By now this example of a model should look familiar, so Im not going to explain it all again. If you
need to refresh your knowledge, review the earlier models.
We need to add a few things, however. Lets start with adding the following to the use statements.
Gist:
Profile Use Statements
From book:
use
use
use
use
use
use
yii\db\ActiveRecord;
common\models\User;
yii\helpers\Url;
yii\helpers\Html;
yii\helpers\ArrayHelper;
yii\db\Expression;
Next up we have an addition to our rules, and dont worry, we will add the actual getGenderList
method that we are calling here at the bottom of the class:
[['gender_id'],'in', 'range'=>array_keys($this->getGenderList())],
And also, we have a rule to add to format the date correctly for birthdate:
[['birthdate'], 'date', 'format'=>'Y-m-d'],
A reader wrote in to tell me that he couldnt get the above to work unless he added the php prefix:
[['birthdate'], 'date', 'format'=>'php:Y-m-d'],
I couldnt reproduce his issue, but I thought I would mention this in case you run into a similar
problem. We will do some additional formatting to the date in chapter 8, so well revisit the date
format again in that chapter.
Moving down the class, we need to add some attribute labels for the relationship methods under
the attributeLabels method, so we can use them later on our widgets throughout the app. We dont
need to go too in depth at this point, other than to say we use the magic syntax of the method name
and that is used by Yii::t method, which is the translate method of the app and will make the label
available across the entire application:
Gist:
Attribute Labels Addition
From book:
93
Since our Profile model deals with DATETIME on several fields, lets add the behaviors method that
automatically inserts our timestamp.
Gist:
Profile Behaviors
From Book:
/**
* behaviors to control time stamp, don't forget to use statement for expression
*
*/
Lets make sure we have the appropriate use statement at the top of the file:
use yii\db\Expression;
94
Reminder
Just a reminder. The code in behaviors is not formatted exactly like the one you see in your
IDE. The reason is that PDF and other formats break the line with a wordwrap and insert
special characters that mess up the code, so I have to proactively format the code so the
line doesnt break. It doesnt always look pretty, but at least the code will function.
Now we move on to relationships. The getGender relationship is already there, generated by Gii.
You can get the other ones:
Gist:
Profile Relations
From book:
/**
* @return \yii\db\ActiveQuery
*/
/**
* get list of genders for dropdown
*/
/**
* @return \yii\db\ActiveQuery
*/
/**
* @get Username
*/
/**
* @getUserId
*/
/**
* @getUserLink
*/
95
96
/**
* @getProfileLink
*/
The only two methods that we havent seen an example of before are the last two. So for brevitys
sake, I will skip over the ones that we already understand and focus on the new ones. However, we
do need to make sure we have included the use statement at the top for the ArrayHelper class:
use yii\helpers\ArrayHelper;
We can utilize the getGenderList method to impose the restriction on values in the model like we
did on the user model:
[['gender_id'],'in', 'range'=>array_keys($this->getGenderList())]
Both getUserLink and getProfileIdLink do the same type of thing. They are methods that create links
to the related user and to the profile id of the user. We use these in some of our UI later and its a
neat way to create links that relate the models. Dont worry if you dont fully get it, you will when
we work on that part.
<?php
namespace frontend\models;
use
use
use
use
use
use
use
Yii;
yii\db\ActiveRecord;
common\models\User;
yii\helpers\Url;
yii\helpers\Html;
yii\helpers\ArrayHelper;
yii\db\Expression;
/**
* This is the model class for table "profile".
*
* @property string $id
* @property string $user_id
* @property string $first_name
* @property string $last_name
* @property string $birthdate
* @property integer $gender_id
* @property string $created_at
* @property string $updated_at
*
* @property Gender $gender
*/
class Profile extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
{
return 'profile';
}
/**
* behaviors
*/
public function behaviors()
{
97
return [
'timestamp' => [
'class' => 'yii\behaviors\TimestampBehavior',
'attributes' => [
ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
],
'value' => new Expression('NOW()'),
],
];
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['user_id', 'gender_id'], 'required'],
[['user_id', 'gender_id'], 'integer'],
[['gender_id'],'in', 'range'=>array_keys($this->getGenderList())],
[['first_name', 'last_name'], 'string'],
[['birthdate'], 'date', 'format'=>'Y-m-d'],
[['birthdate', 'created_at', 'updated_at'], 'safe']
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'user_id' => 'User ID',
'first_name' => 'First Name',
'last_name' => 'Last Name',
'birthdate' => 'Birthdate',
'gender_id' => 'Gender ID',
98
99
{
return $this->hasOne(User::className(), ['id' => 'user_id']);
}
/**
* @get Username
*/
public function getUsername()
{
return $this->user->username;
}
/**
* @getUserId
*/
public function getUserId()
{
return $this->user ? $this->user->id : 'none';
}
/**
* @getUserLink
*/
public function getUserLink()
{
$url = Url::to(['user/view', 'id'=>$this->UserId]);
$options = [];
return Html::a($this->getUserName(), $url, $options);
}
/**
* @getProfileLink
*/
public function getProfileIdLink()
{
$url = Url::to(['profile/update', 'id'=>$this->id]);
$options = [];
return Html::a($this->id, $url, $options);
100
101
}
}
I will probably mention it a hundred times in this book, but the style of the code is set to avoid
wordwrapping in PDF, so that is why it appears the way it does in the book. For a cleaner
representation of the code, reference the Gist. In either case, the code has been tested and is working.
/**
* @getProfileLink
*
*/
102
{
$url = Url::to(['profile/view', 'id'=>$this->profileId]);
$options = [];
return Html::a($this->profile ? 'profile' : 'none', $url, $options);
}
So the getProfile method is very familiar at this point, were simply mapping user_id on the profile
table to the id column on the user table. This is how the two are associated.
The getProfileId method is a ternary statement that shows none if the user does not have a profile.
So when we call this method in the UI, and the user doesnt have a profile, we have an answer instead
of null. Returning null when another answer is expected can lead to errors and a lot of debugging
time. Its best to account for null when you can.
We need the getProfileId method to feed into our getProfileLink method in that methods Url::to
method. The getProfileLink is just like the getUserLink on the profile model. It utilizes the Url helper
class and the Html helper class, so we have to include the use statements at the top of the file:
use yii\helpers\Url;
use yii\helpers\Html;
103
/**
* @getUserLink
*
*/
One last bit of work on the User model. We need to add labels for all of the following methods via
the attributeLabels method.
Gist:
Attribute Labels
From book:
/* Your model attribute labels */
104
Attribute labels tell Yii 2 how to display your attributes when they appear on the site. In some
cases, as we have them used here, the attribute is the name of a method. For example, roleName is a
label for the getRoleName method, which we made for this User model. It is paired with the Yii::t()
method, which is the translate method for the entire app, so setting it here should make it appear
this way everywhere.
If this seems a little confusing, dont worry about it now. It will become more clear when you see
the attribute labels appearing in view files and widgets. I will reference it again when we come to
that point.
Yii;
yii\base\NotSupportedException;
yii\db\ActiveRecord;
yii\db\Expression;
yii\web\IdentityInterface;
yii\helpers\Security;
backend\models\Role;
backend\models\Status;
backend\models\UserType;
frontend\models\Profile;
yii\helpers\ArrayHelper;
yii\helpers\Url;
yii\helpers\Html;
/**
* User model
*
* @property integer $id
* @property
* @property
* @property
* @property
* @property
* @property
* @property
* @property
* @property
* @property
*/
string $username
string $password_hash
string $password_reset_token
string $email
string $auth_key
integer $role
integer $status
integer $created_at
integer $updated_at
string $password write-only password
/**
* @inheritdoc
*/
105
/**
* @inheritdoc
*/
];
}
/* Your model attribute labels */
106
/**
* @inheritdoc
*/
/**
* @inheritdoc
*/
/**
* Finds user by username
*line break for wordwrap in pdf, should be single line
* @param string $username
107
108
* @return static|null
*/
/**
* Finds user by password reset token
*
* @param string $token password reset token
* @return static|null
*/
public static function findByPasswordResetToken($token)
{
if (!static::isPasswordResetTokenValid($token)) {
return null;
}
return static::findOne([
'password_reset_token' => $token,
'status_id' => self::STATUS_ACTIVE,
]);
}
/**
* Finds out if password reset token is valid
*
* @param string $token password reset token
* @return boolean
*/
public static function isPasswordResetTokenValid($token)
{
if (empty($token)) {
return false;
}
$expire = Yii::$app->params['user.passwordResetTokenExpire'];
$parts = explode('_', $token);
$timestamp = (int) end($parts);
return $timestamp + $expire >= time();
}
/**
* @inheritdoc
*/
/**
* @inheritdoc
*/
/**
* @inheritdoc
*/
/**
109
* Validates password
*
* @param string $password password to validate
* @return boolean if password provided is valid for current user
*/
/**
* Generates password hash from password and sets it to the model
*
* @param string $password
*/
/**
* Generates "remember me" authentication key
*/
/**
* Generates new password reset token
* 2 lines to avoid wordwrapping, should be one line
*/
110
/**
* @getRole
*
*/
/**
* @getRoleName
*
*/
111
/**
* @getRoleList
*/
/**
* @getStatus
*
*/
/**
* @getStatusName
*
*/
/**
* @getStatusList
*/
112
/**
* @getProfile
*
*/
/**
* @getProfileId
*
*/
/**
* @getProfileLink
*
*/
113
/**
* @getUserType
*/
/**
* @getUserTypeName
*
*/
/**
*@getUserTypeList
*/
/**
* @getUserTypeId
114
*
*/
/**
* @getUserIdLink
*
*/
/**
* @getUserLink
*
*/
115
116
Summary
Commit!
This chapter was a beast. We created 5 new tables to add to our data structure. We created 5 new
models and updated the User model. We havent done that much custom coding yet, we primarily
relied on Gii to create our models for us. Then we added relationship methods, rules, use statements,
behaviors and various other odds and ends to unlock the power of Yii 2.
A lot of the relationship methods we added will go a long way towards building an intuitive UI
that allows us to manage users and control their access to various parts of the application. We are
building the application with eye towards code reuse and extensibility. We want to make a template
that would be a good starting point for any application.
We havent seen how it all translates into an application yet, but dont worry we will. And youll
get to see just how easy Yii 2 makes this for us.
Value Helpers
I could have put all of these methods in a single Utilities or Helpers class, but recently I read Clean
Code by Robert Martin, and one of the results of reading books on programming is that you try to
adopt the principles that you like. In this case, I really like the idea of getting semantic help from class
names and method names. So I decided to go for smaller helper classes that were more descriptive.
For example, we could have a class named ValueHelpers. We could use it to return values with
methods named as follows:
roleMatch - see if the users role matches the specified role
118
These methods are going to be the building blocks of our access control, which will help us build a
production-ready template.
I could have provided a much simpler solution or simply relied on Yii 2s RBAC, but I didnt do that
because thats not a fit for this template. I want to provide a robust enough foundation for us to be
able to continue to build upon and improve as our needs evolve.
The downside is that this is a bit more complicated than just giving a quick this is how it works tour
of Yii 2. The upside is that you get closer to the code that is going to manage your access control,
and therefore, when you need to customize it in the future in some way we cant anticipate now,
you will have a much better understanding on how to do it.
Also, as we go along creating our helper methods, we will get use Yii 2s Active Record implementation to find and return results. We will also be using raw sql in some cases, so you get to see the
contrast between the different ways of accessing data from your database.
Lets go ahead and create ValueHelpers.php in the common/models folder. For the sake of consistency and to not have to bounce around all over the place, Im simply going to give you the entire
class. Then we will discuss the methods.
Gist:
ValueHelpers
From book:
<?php
namespace common\models;
use
use
use
use
use
yii;
backend\models\Role;
backend\models\Status;
backend\models\UserType;
common\models\User;
class ValueHelpers
{
public static function roleMatch($role_name)
{
$userHasRoleName = Yii::$app->user->identity->role->role_name;
return $userHasRoleName == $role_name ? true : false;
}
public static function getUsersRoleValue($userId=null)
{
if ($userId == null){
$usersRoleValue = Yii::$app->user->identity->role->role_value;
return isset($usersRoleValue) ? $usersRoleValue : false;
} else {
$user = User::findOne($userId);
$usersRoleValue = $user->role->role_value;
return isset($usersRoleValue) ? $usersRoleValue : false;
}
}
public static function getRoleValue($role_name)
{
$role = Role::find('role_value')
->where(['role_name' => $role_name])
->one();
return isset($role->role_value) ? $role->role_value : false;
}
public static function isRoleNameValid($role_name)
119
{
$role = Role::find('role_name')
->where(['role_name' => $role_name])
->one();
return isset($role->role_name) ? true : false;
}
120
121
Please dont worry about memorizing these methods. Its not necessary for you to do that. We are
however going to review them in detail, so it will give you more of an overview of how all this will
work and to introduce you to Yii 2s Active Record.
Anyway, you can see we have 7 fairly simple methods that return values associated with the models
we created.
So for example, if I want to know the value of Admin, I can call:
getRoleValue(Admin);
Role Records
The answer is 20. So when I want to control access to the backend, for example, I have written a
method to restrict access to users that have a minimum role value of 20. Or I have one to match 20
exactly if I wanted to just have one kind of role have access.
We are not making that decision or building that method yet, we are just anticipating that we need
the value for decisions we will make in the future.
One thing you might want to know is what is the purpose of role_value, why not just use the id
primary key? The reason I added a role_value column, however, was to give us more flexibility in
the design of the system. You will see how that plays out later.
Youll also notice that our methods are public static methods, which means they can be called as so:
ValueHelpers::getRoleValue('Admin');
at the top of your file, you can use that method like that.
Weve done our best with the helper methods to make them as semantically pleasing and easy to
understand as possible. This makes understanding the purpose easier when you come back to it after
a period of time.
These are simple methods, but lets step through them. Here is the first one:
122
Were calling it roleMatch as if posing a question, so it makes sense that it returns true or false.
We could use that as follows:
if(ValueHelpers::roleMatch(Admin'){
//then allow the user to do this
}
For the method to do that calculation, we rely on the relationship between the User and Role models
in the following:
Yii::$app->user->identity->role->role_name;
So, first thing to note about that is the user attributes are always available to us via a call like so:
Yii::$app->user->identity->username;
That will return the username of the current user, who would have to be logged in for this to work.
You also need:
use yii;
That must go at the top of any file in which you wish to access the user with the Yii::$app.
So, getting back to:
Yii::$app->user->identity->role->role_name;
We are using magic get syntax for the getRole method on the User model, so that is why you see
->role instead of ->getRole(). Once the models are tied together via the relationship, as we put in
place in the previous chapter, we can access the properties of the related model, in this case, its
role_name, which belongs to Role.
You can see how much power is packed into one line of code. Its just incredible. This makes the
framework a pleasure to work with.
To keep the lines short, especially for this book, Im assigning that to a variable:
123
$userHasRoleName = Yii::$app->user->identity->role->role_name;
From there we are doing a simple equal comparison via ternary to see if it matches the role name
that was handed in and we are done:
return $userHasRoleName == $role_name ? true : false;
By setting $userId to null in the signature, we are giving ourselves the option of handing in a value
for that or not. In the case of a logged in user, we dont need to hand in the $userId, we already have
it.
So in that case, we get:
124
if ($userId == null){
$usersRoleValue = Yii::$app->user->identity->role->role_value;
return isset($usersRoleValue) ? $usersRoleValue : false;
Again the relationship between User and Role is already there and easy for us to use. We check to
see if $usersRoleValue is set, and if not return false. Otherwise return $usersRoleValue.
But in some cases, the user might not be logged in, in which case we need to hand in the user id. Its
just slightly more complicated. We have to create an ActiveRecord instance of the user, so we can
use the relationship to find the $usersRoleValue:
$user = User::findOne($userId);
Obviously ActiveRecord formatting makes setting up a query easy. The Yii 2 guide has many
examples on how to use ActiveRecord.
So rather than reproducing whats in the Yii 2 guide, I will refer you to the guide with the suggestion
you take a few minutes to look it over:
Yii 2 Active Record
Dont worry though, I wont leave you stranded. I will continue to explain everything we use fully.
Once we create an instance of the User, we use our relationship to role to finish this up:
$usersRoleValue = $user->role->role_value;
return isset($usersRoleValue) ? $usersRoleValue : false;
So we had to jump through a few hoops to get our users role value, but its worth it. It will give us
flexibility in building out the rest of our access control. Anyway by the time we are using our helper
methods in our controllers, you will be very happy we did all this.
Next, we simply want a role value without the user. We hand in the name of the role and use an
instance of ActiveRecord to access it:
125
In this ActiveRecord call, I switched to using the find method instead of findOne because its not a
primary key and findOne is a shortcut for searching on a primary key.
Also, just as a heads up, in order to use ActiveRecord to access results, you have to include use
statements for the models you want to return. You can see we included the following at the top of
the file:
use
use
use
use
use
yii;
backend\models\Role;
backend\models\Status;
backend\models\UserType;
common\models\User;
Next we have a quick check to see if the role name is valid, this helps prevent programming errors:
public static function isRoleNameValid($role_name)
{
$role = Role::find('role_name')
->where(['role_name' => $role_name])
->one();
return isset($role->role_name) ? true : false;
}
In implementing access control, having control over a users status is also important:
126
The statusMatch method is exactly like the roleMatch method at the top of the class, using the same
technique with relationships, so review that method if you are uncertain on how this works.
Next:
public static function getStatusId($status_name)
{
$status = Status::find('id')
->where(['status_name' => $status_name])
->one();
return isset($status->id) ? $status->id : false;
}
Here we are just handing in the name of status to return via ActiveRecord the id. This will come
in handy when we want to remove the status constant from the user model. But dont worry about
that now.
And finally, we have:
public static function userTypeMatch($user_type_name)
{
$userHasUserTypeName = Yii::$app->user->identity->userType->user_type_name;
return $userHasUserTypeName == $user_type_name ? true : false;
}
Like roleMatch and statusMatch, it simply returns true or false if the users userType matches the
one we specify in the signature.
127
Permission Helpers
As we imagine how we would use our helpers, knowing value isnt enough. If we are going to control
access to areas of the application, we would want helpers that use value and define permission. So
we are going to create a PermissionHelpers class in common/models.
Go ahead and create PermissionHelpers.php in common/models.
Gist:
PermissionHelpers
From book:
<?php
namespace common\models;
use
use
use
use
common\models\ValueHelpers;
yii;
yii\web\Controller;
yii\helpers\Url;
class PermissionHelpers
{
public static function requireUpgradeTo($user_type_name)
{
if (!ValueHelpers::userTypeMatch($user_type_name)) {
return Yii::$app->getResponse()->redirect(Url::to(['upgrade/index']));
}
}
public static function requireStatus($status_name)
{
return ValueHelpers::statusMatch($status_name);
}
public static function requireRole($role_name)
128
{
return ValueHelpers::roleMatch($role_name);
}
public static function requireMinimumRole($role_name, $userId=null)
{
if (ValueHelpers::isRoleNameValid($role_name)){
if ($userId == null) {
$userRoleValue = ValueHelpers::getUsersRoleValue();
}
else {
$userRoleValue = ValueHelpers::getUsersRoleValue($userId);
}
return $userRoleValue >=
ValueHelpers::getRoleValue($role_name) ? true : false;
} else {
return false;
}
}
public static function userMustBeOwner($model_name, $model_id)
{
$connection = \Yii::$app->db;
$userid = Yii::$app->user->identity->id;
$sql = "SELECT id FROM $model_name
WHERE user_id=:userid AND id=:model_id";
129
$command = $connection->createCommand($sql);
$command->bindValue(":userid", $userid);
$command->bindValue(":model_id", $model_id);
if($result = $command->queryOne()) {
return true;
} else {
return false;
}
}
}
Ok, great, only 5 methods and we will see how they utilize ValueHelpers to help us control access in
ways that will be important to us. Were not going too deep, however, we will get into more detail
when we actually use the methods in the application.
Remember, all of these helper methods are public static so they can be called like so:
PermissionHelpers::requireUpgradeTo(Paid);
And so we jumped right into our first example. If you do build an application that has an area for
users of a different type, like in the example above, Paid users, then this is perfect for your controller.
It tests the current user to see if their user type matches what you handed into the method.
It does this using our handy ValueHelpers::userTypeMatch method. So already we are reusing code
from our ValueHelpers class, you have to love that.
Ok, back to the method. If the user type matches, fine, continue, if not, redirect to upgrade page. Of
course if you have a different destination in mind, just put the controller/action in the method.
If you have more than one upgrade or redirect page, you could rewrite the method to take a second
argument, such as:
130
In that case, you would just hand in the controller action as a string upgrade/salespitch as an
example, in the second argument. Anyway, I only included the method that has the redirect
hardcoded in because Im not anticipating the application being more complicated than that and
I like the simpler syntax of the single argument.
The next 2 methods, requireStatus and requireRole simply call existing statusMatch and roleMatch
ValueHelper methods and are not absolutely necessary, since they dont do anything new.
The reason they exist is so that I can have a cosmetic and syntactic consistency that makes it easy
for me to work with my access control methods.
You can see the theme emerging of PermissionHelpers::requireSomething as a permission manager.
Its just super simple for me to remember, work with, and maintain.
Anyway, both of those methods require an exact match to return true.
Next, I did a requireMinimumRole method with the >= operator, so if you wanted for example
Admin and SuperUser to be able to access the backend, you could control access to the backend
with this method.
First we check to see isRoleNameValid:
if (ValueHelpers::isRoleNameValid($role_name)){
if that fails, immediately return false. If were good, we then check to see if we have handed in a
$userId:
if ($userId == null) {
$userRoleValue = ValueHelpers::getUsersRoleValue();
}
If its null, call getUsersRoleValue without handing in the $userId and set the result to $userRoleValue. This assumes the user is logged in and the getUsersRoleValue method will handle that
scenario.
131
If its not null, and we handed in a value for $userId, then getUsersRoleValue method will handle
that scenario as well.
Then we just do a simple check with >= to see if the user meets the requireMinimumRole that we
handed in:
return $userRoleValue >= ValueHelpers::getRoleValue($role_name) ? true : false;
This one had a few moving parts, which might be harder to follow, but if you want to call it, look
how intuitive the syntax is:
PermissionHelpers::requireMinimumRole('Admin');
Typically, you are going to use that as part of an if statement, since it returns true or false.
Even though we havent completely explained it, you are starting to get some idea of how we are
answering the question, How we will control access to the backend?
Dont worry if you dont completely get it now, you will when you see it in action.
The last method, userMustBeOwner, takes 2 parameters, model name and model id. Then it performs
a query to see if the current user is the owner of that specific record.
You can use the method as an example of how to do a raw query:
public static function userMustBeOwner($model_name, $model_id)
{
$connection = \Yii::$app->db;
$userid = Yii::$app->user->identity->id;
$sql = "SELECT id FROM $model_name
WHERE user_id=:userid AND id=:model_id";
$command = $connection->createCommand($sql);
$command->bindValue(":userid", $userid);
$command->bindValue(":model_id", $model_id);
if($result = $command->queryOne()) {
return true;
} else {
return false;
}
}
132
If the user owns the record, it returns true, if not returns false. When using it, the syntax would look
typically like this:
if (PermissionHelpers::userMustBeOwner (profile, $model->id)) {
//do something
}
So how would you use this? Well, lets say you have a group of posts or other records that are visible
to everyone, but only the author can update or delete them and you want to make the navigation
visible only to the owner of the record. So the do something in the above example, could be show
the navigation in a view file. We will use this exact example later in the book.
I wrote it this way because I like the syntax and I felt like this would be a good way to work with
it. But keep in mind there are typically many ways to accomplish the same thing and a helper is not
always necessary. I like to use them because it also gives me consistency in the coding, but this is
definitely one area where you have to use your own judgement.
Record Helpers
Ok, so we have one last helper file, RecordHelpers. Lets go ahead and create RecordHelpers.php in
common/models and put the following contents in the file:
Gist:
RecordHelpers
From book:
<?php
namespace common\models;
use yii;
class RecordHelpers
{
public static function userHas($model_name)
{
$connection = \Yii::$app->db;
133
$userid = Yii::$app->user->identity->id;
$sql = "SELECT id FROM $model_name WHERE user_id=:userid";
$command = $connection->createCommand($sql);
$command->bindValue(":userid", $userid);
$result = $command->queryOne();
if ($result == null) {
return false;
} else {
return $result['id'];
}
}
This class has only one method. What I have planned for our application is a user profile and I want
a Profile link that when you click on it, figures out whether or not the user has a profile or if they
need to create one.
I wanted to keep the syntax in my controller very intuitive and have the result formatted to either
false or the record id. That way if it comes back false, I can have the user create the record, and if it
returns the id of the record because the user already has one, I can render that view. Something like:
If ($already_exists = RecordHelpers::userHas('profile') {
// show profile with id with value of $already_exists
} else {
// go to form to create profile
}
This kind of syntax makes it incredibly easy to understand what is happening here. If the if statement
returns a record id, show the profile with that record id, which is now referenced by the variable
$already_exists. If it comes back false, go to the create form.
134
I also wrote it so I could use it with other models, I just need to hand in the model name as string.
You should note that this method is written to return a single record, you would have to modify it
if there were a possibility that the user could have multiple records, multiple profiles for example.
Summary
Commit!
Ok, by now you are realizing that learning Yii 2 is fun, but also is a lot of work. This is a huge
framework, elegant and powerful, capable of doing so many things. Weve done a lot already. We
set up the application, we briefly reviewed the MVC architecture, and we modified the User model.
We also built 5 new models and put in place their relationships to each other and their relationship
to the User model, and we built a number of helper methods to make coding easier when we dig
further into the application, especially for access control.
Weve done all of this, and yet our application currently does nothing more than it did when you
installed it. Thank you for your patience. In the next chapters, we will begin adding features to our
application.
Yii;
common\models\LoginForm;
frontend\models\PasswordResetRequestForm;
frontend\models\ResetPasswordForm;
frontend\models\SignupForm;
frontend\models\ContactForm;
yii\base\InvalidParamException;
yii\web\BadRequestHttpException;
yii\web\Controller;
yii\filters\VerbFilter;
yii\filters\AccessControl;
It uses quite a few models and we will see this in action. Next we have class declaration:
class SiteController extends Controller
136
You can see it extends Controller. When you have time, browse through Controller, it will give you a
better idea of how things work, but be forewarned, the framework code is sometimes hard to follow,
especially for beginners. The code you see on the surface is much friendlier than what you will see
below.
Behaviors
Next we have something familiar, a behaviors method. We saw those on some of our models with
TimeStamp behavior. Our controllers use AccessControl behavior:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['logout', 'signup'],
'rules' => [
[
'actions' => ['signup'],
'allow' => true,
'roles' => ['?'],
],
[
'actions' => ['logout'],
'allow' => true,
'roles' => ['@'],
],
],
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'logout' => ['post'],
],
],
];
}
This is Yii 2s out-of-the-box method for controlling access to the site, and it mainly controls logged
in or guest. The ? is guest and @ is logged in. So now that we know that, we can see Yii 2s
incredibly intuitive syntax at work. But lets break it down just to be sure. First thing:
Name the behavior:
137
return [
'access' => [
This is called access, but you could call it any string and it would work the same. Next comes class:
'class' => AccessControl::className(),
This is just telling us what class to apply. Next we see what actions to apply the behavior to:
'only' => ['logout', 'signup'],
So these rules are only going to apply to logout and signup. Next come the rules, and this is where
we can apply them to specific actions:
[
'actions' => ['signup'],
'allow' => true,
'roles' => ['?'],
],
[
'actions' => ['logout'],
'allow' => true,
'roles' => ['@'],
],
So, the first rule says signup, true, guest, in other words, guests are allowed to signup. Second rule
says, logout, true, logged in user, in other words logged in users are allowed to access the logout
action. Since we specified in the only part of the method, all other actions are not controlled by
these rules.
As we go through the other actions, we will see why this make sense.
Actions
The first method below the behaviors is actions:
138
This is a configuration method, telling us which class to use for error and which class to use for
Captcha. The configuration in actions makes these actions available to the controller. So if you want
to use Captcha for something, you need it configured in the controller, like it is above. We also need
to set it up as a widget, which we will see in action soon.
Index Action
Lets move on to actionIndex:
public function actionIndex()
{
return $this->render('index');
}
Yes! Things had to get simpler sooner or later This simply calls the render method to the view, in
this case index.
This gives us an opportunity to refresh ourselves on how the routing works. The route to index looks
like this:
yii2build.com/index.php?r=site/index
Since we left the ugly urls in place, its very explicit. Index.php is the bootstrap page, everything
goes through that doorway. The r for route, = site/index. In this case, site is the controller, index is
the action. If you leave the action off a controller, it will look for an index action and default to it. If
there is no index action, it will return an error.
Also a quick note, the default to the site is set to the one above, so if you just type in the domain,
yii2build.com, that is the route you will get.
The action in most cases will render a view, using the syntax we see on the index action above,
and that is how we get to see the page. At. any rate, you should have a sense now how the
controller/action moves us through the site.
139
Login Action
The next method on the controller is actionLogin:
public function actionLogin()
{
if (!\Yii::$app->user->isGuest) {
return $this->goHome();
}
$model = new LoginForm();
if ($model->load(Yii::$app->request->post()) && $model->login()) {
return $this->goBack();
} else {
return $this->render('login', [
'model' => $model,
]);
}
}
Ok, great, well see how the user logs in. The first thing that happens is the test to see if they are
already logged in:
if (!\Yii::$app->user->isGuest) {
return $this->goHome();
}
It does this by checking to see if user is not guest. If they are not a guest, that means they are already
logged in and in this case, we send them to the homepage.
We spoke a little about the LoginForm model, when we were on Modifying the User model,
but it bears taking a closer look, so we can understand exactly how this works. Its located in
common/models, so that is why the first block looks like:
140
<?php
namespace common\models;
use Yii;
use yii\base\Model;
Remember, this model extends Model, not User, so we have to declare the properties. We are
defaulting $rememberMe to true, this sets the flag on the form for the cookie. We default $_user
to false and well see why in a moment.
Next we have a rules method:
public function rules()
{
return [
// username and password are both required
[['username', 'password'], 'required'],
// rememberMe must be a boolean value
['rememberMe', 'boolean'],
// password is validated by validatePassword()
['password', 'validatePassword'],
];
}
Yii 2 has provided comments that explain the validators we are using. You can see password uses
the validatePassword method as its validator:
141
Fairly intuitive method, if no errors, great get the user, otherwise declare error.
Next we have the login method itself as of Yii 2.0.3:
/**
* 2 lines in return statement to avoid wordwrap
*/
public function login()
{
if ($this->validate(){
return Yii::$app->user->login($this->getUser(),
$this->rememberMe ? 3600 * 24 * 30 : 0);
} else {
return false;
}
}
Ok, slightly more complicated. $this->validate() is calling the validate method of Model. and we also
call getUser from the LoginForm model, which we will see in a moment. If we validate, we return
the login method of user available to us from the Yii::$app.
This is actually a reference the User model in vendor/yiisoft/yii/web/User. This is the class Yii 2 uses
to manage identity and login and there is a rather complicated login method there. It gets a little
confusing to have multiple models and multiple methods with the same name, but that is the nature
of the beast. We can see that it takes the in the user and remember me setting in the signature and
then logs in. I wont go further into that login method since its beyond the scope of this discussion
and definitely not for beginners, but you can check it out for yourself if you wish.
At least it makes the code in the LoginForm model seem very intuitive by comparison. Anyway, it
works, and it logs in the user and sets the remember me cookie if that flag has been set in the form.
If something fails, it returns false, and usually validation messages will be sent to the view telling
the user what the problem is. On the surface, its very simple.
Ok, last method of this model:
142
Since we know the private model property $_user defaults to false, the condition in the if statement
is going to be met if this method has not already been run. So if there is no username in $_user, then
it uses a static method of the User model to return a model instance of the desired user and set it to
$_user.
In order for this to work, the LoginForm model obviously has to get the values for its properties
from the post, so it knows who $this->username refers to, and it can look up the user and set it to
$_user. So lets see how we get the post data by returning now to the actionLogin method of the
SiteController and picking up where we left off. So after calling a new instance of the LoginForm
model, we get:
if ($model->load(Yii::$app->request->post()) && $model->login()) {
return $this->goBack();
} else {
return $this->render('login', [
'model' => $model,
]);
If we can load the post data, which will validate according to the model as we described earlier and
if it can utilize the models login method, it will return the user to whatever page they were on using:
return $this->goBack();
143
You can also see here that its passing an instance of $model to the view, which we know in this case
is LoginForm model that we set earlier with:
$model = new LoginForm();
That makes the model available to the view. And thats it for login, hopefully you got a good
understanding of how that works.
Logout Action
The actionLogout method is significantly simpler:
public function actionLogout()
{
Yii::$app->user->logout();
return $this->goHome();
}
It uses the logout method of the user model buried deep in the bowels of Yii 2 and sends the user to
the index page via goHome().
Contact Action
The next method routes us to a simple contact page,with the actionContact method, but there are
some interesting things in here.
public function actionContact()
{
$model = new ContactForm();
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
if ($model->sendEmail(Yii::$app->params['adminEmail'])) {
Yii::$app->session->setFlash('success',
'Thank you for contacting us. We will respond to you as soon as possible.');
} else {
Yii::$app->session->setFlash('error', 'There was an error sending email.');
144
}
return $this->refresh();
} else {
return $this->render('contact', [
'model' => $model,
]);
}
}
$name;
$email;
$subject;
$body;
$verifyCode;
/**
* @inheritdoc
*/
145
So we have the namespace, the use statements, the class properties, and the first method, rules.
Notice we have an attribute verifyCode. We will use the captcha validator on this attribute. If you
recall, the captcha class was configured into the controller through the actions method, so it is
available as a controller action. Cool stuff.
Captcha
Lets take a minute to discuss captcha in detail, since its a very useful feature to include in your
applications. The good news is that Yii 2 makes this very easy to implement.
Using it on SiteController is default behavior, so there is a little more to it if you use on a different
controller. However, its still very simple.
If you need to implement captcha on anywhere else on your future applications, these are the steps
for implementing captcha:
Step 1. Configure captcha into the new controller via actions method.
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
];
}
146
Step 2. Include a captcha validator on form model rules with extra parameter for controller/captcha.
This is not necessary when using it with SiteController, but in all other cases, we must define it.
For example lets say we want to use it on the contact action of a PagesController instead of
SiteController. Lets imagine we are using the existing ContactForm model and modifying the rules
method. It would look like this:
['verifyCode', 'captcha', 'captchaAction' => 'pages/captcha']
Note: Make sure there is a matching $verifyCode property on the form model class. See the current
ContactForm model for example.
Step 3. Include widget on the view with same captchaAction parameter in widget config:
<?= $form->field($model, 'verifyCode')->widget(Captcha::className(), [
'captchaAction' => 'pages/captcha',
'template' => '<div class="row"><div class="col-lg-3">
{image}</div><div class="col-lg-6">{input}</div></div>',
]) ?>
Step 4. Include an access rule in behaviors method to allow captcha in the controller:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['captcha'],
'rules' => [
[
'actions' => ['captcha'],
'allow' => true,
'roles' => ['?', '@'],
],
],
],
];
}
147
Adding the access rule is not necessary on SiteController, but will be needed on any other controller.
Step 5. Clear cache. It may be necessary to clear your browser cache after making changes.
This might seem like a lot of steps, but its actually very easy to implement. I dont know of any
other PHP framework that makes it this easy. And dont worry if you havent instantly memorized
this, I dont have a photographic memory either. Just use this as a reference and know that its here
for you.
Anyway, we jumped ahead, were not looking at the view yet, plus we have more cool stuff
happening here in a moment.
First lets continue with the ContactForm model. The attributeLabels method is next:
public function attributeLabels()
{
return [
'verifyCode' => 'Verification Code',
];
}
This is the label that will appear on the form in the view. No further explanation needed there.
And finally, the sendEmail method:
public function sendEmail($email)
{
return Yii::$app->mailer->compose()
->setTo($email)
->setFrom([$this->email => $this->name])
->setSubject($this->subject)
->setTextBody($this->body)
->send();
}
This is calling the compose method from the mailer configured in the application which should be
Swiftmailer. When you are in development mode, which is how we setup our application on init in
setup, emails will be sent to frontend/runtime/mail.
Note: The folder will not exist until you create a record, which you can do by testing the contact
form. So you can try your contact form to see if puts an email there. For more info on configuration
options with Swiftmailer, you can check the Yii 2 guide:
Yii 2 Email Guide
And thats it for our ContactForm model. Lets return now to SiteController and its actionContact
method. So we called the instance of ContactForm and set it to $model. Then we have:
148
So, if we get the post data and process and validate through the form model, and if the email can
be sent, we instruct the method to send a flash message. A flash message, which is text that will
appear in the view, gets sent from the controller to the view via session. So here we have the setFlash
method. Thats all you need to send a flash message, Yii 2 will do the rest.
Note that in any event in this method, it will stay on the contact page. In cases where it processes
the form, successfully or not, it refreshes the page, otherwise it shows the form, which is the same
thing, but without the flash messages.
Its worth it now to take a look at the view as we have a couple of interesting things going on,
including the use of captcha as verify method.
<?php
use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
use yii\captcha\Captcha;
/* @var $this yii\web\View */
/* @var $form yii\bootstrap\ActiveForm */
/* @var $model \frontend\models\ContactForm */
$this->title = 'Contact';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="site-contact">
<h1><?= Html::encode($this->title) ?></h1>
<p>
If you have business inquiries or other questions, please fill out the
following form to contact us. Thank you.
</p>
<div class="row">
<div class="col-lg-5">
<?php $form = ActiveForm::begin(['id' => 'contact-form']); ?>
<?= $form->field($model, 'name') ?>
<?= $form->field($model, 'email') ?>
<?= $form->field($model, 'subject') ?>
<?= $form->field($model, 'body')->textArea(['rows' => 6]) ?>
<?= $form->field($model, 'verifyCode')
->widget(Captcha::className(), [
'template' => '<div class="row"><div class="col-lg-3">
{image}</div><div class="col-lg-6">{input}</div></div>',
]) ?>
<div class="form-group">
<?= Html::submitButton('Submit', ['class' => 'btn btn-primary',
'name' => 'contact-button']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
</div>
</div>
149
150
You can see we start with our use statements and some comments that tell us what variables we
access:
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
use yii\captcha\Captcha;
/* @var $this yii\web\View */
/* @var $form yii\widgets\ActiveForm */
/* @var $model \frontend\models\ContactForm */
This is handy for reference because its easy to get $this and $model confused. But not to worry, you
can just look at the top of the file.
Next we get title and breadcrumbs:
$this->title = 'Contact';
$this->params['breadcrumbs'][] = $this->title;
?>
We use the params method to send the breadcrumb parameter to the layout file(frontend/views/layouts/main.php),
which calls it via the Breadcrumbs widget, so it appears at the top of the page.
Also, Notice the closing php tag below. In our models and controllers, we dont use closing ?> tags.
In our views, not only do we use closing ?> tags, but we have to make sure we open and close php
correctly as we are interspersed with HTML.
Next we get a div, no php:
<div class="site-contact">
Tip
Im a big fan of red php tags, especially in views. I cant control that color in this book, but
thats just a tip for your own coding. That color really helps it pop out against Html code.
Also note the shorter <?= opening php tag. This is the short version of:
<?php echo
So in our h1, we are echoing the title, within the Html::encode method. This will convert special
characters into html entities. This is why we need to:
151
use yii\helpers\Html;
Then we have a <p> for instructions on filling out the form. This is just plain Html format.
Then we get two divs for our form widget to sit in:
<div class="row">
<div class="col-lg-5">
Note that we cant use ActiveForm without the use statement at the top of the file:
use yii\bootstrap\ActiveForm;
In the configuration array of the widget, we set id contact-form. This tells the form to use the
ContactForm model. So looking over the rest of the code, theres nothing to indicate the form action.
How does it know what action to post to?
This is one the truly awesome features of Yii 2. It knows that the forms location is a view file
called contact.php in a view folder named site. Therefore it knows that it should submit the action
to site/contact. You dont even need to tell it where to post the form. And since weve mentioned
that we set the id of the form to contact-form, it has the model as well, so it has everything it needs
to put this together for you behind the scenes, including validation and processing.
So now just define what fields you want to post:
<?= $form->field($model, 'name') ?>
<?= $form->field($model, 'email') ?>
<?= $form->field($model, 'subject') ?>
<?= $form->field($model, 'body')->textArea(['rows' => 6]) ?>
<?= $form->field($model, 'verifyCode')
->widget(Captcha::className(),
[
'template' => '<div class="row"><div class="col-lg-3">
{image}</div><div class="col-lg-6">{input}</div></div>',
])
?>
Note: As I mentioned above, SiteController is the default controller for captcha, so in this case, we
do not need to specify captcha site/captcha, but in all other uses, you will need to specify the
controller/captcha as gave in the previous example.
Tip
Just another reminder, you need to include the closing ?> tag in views.
152
On the last field, we got a little tricky, we put a widget in the field. In this case, its the Captcha
widget that we have been discussing. There are other widgets that we will use in the future, such as
dropDownList and DatePicker, which are easy to use as well.
Finally, we add the divs for the button and the end of the form:
<div class="form-group">
<?= Html::submitButton('Submit', ['class' => 'btn btn-primary',
'name' => 'contact-button']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
</div>
</div>
I dont know how you feel about that, but to me, that is so simple, its inspiring. All the validation, all
that complicated regex stuff, handled effortlessly. If working on form validation was your favorite
hobby, its time to find a new hobby.
Ok, believe it or not, we are still in a chapter about the site controller. So lets return to that now.
About Action
The next method is actionAbout:
public function actionAbout()
{
return $this->render('about');
}
Signup Action
Lets move on to actionSignup:
153
Here we go, new form model, SignupForm. Just a quick note. I checked the signup.php view and
the id of the form widget was set to form-signup, which I believe is a typo. The convention seems
to be the reverse, so just to check if it would work, I changed it to signup-form, and sure enough,
it still works. So most likely, Yii 2 doesnt care what order you put them in. I, on the other hand,
get nervous about things like that, so Im sticking with one way to do it, which is putting the word
form second. This is also an indication that when creating a form model, you should stick with the
naming convention ExampleForm, for example.
/**
* Signup form
*/
154
Nothing that we havent seen before. Lets look at the rules method:
public function rules()
{
return [
['username', 'filter', 'filter' => 'trim'],
['username', 'required'],
['username', 'unique',
'targetClass' => '\common\models\User',
'message' => 'This username has already been taken.'],
['username', 'string', 'min' => 2, 'max' => 255],
['email', 'filter', 'filter' => 'trim'],
['email', 'required'],
['email', 'email'],
['email', 'unique',
'targetClass' => '\common\models\User',
'message' => 'This email address has already been taken.'],
['password', 'required'],
['password', 'string', 'min' => 6],
];
}
Ok, we have some rules to trim out white space, require our fields, and show us if the username or
email is already in use, examples of the unique validator. Note that on the email and user unique
rules, it lists a target class, along with a response message. This is the only time Ive seen this and
I couldnt find anything in the docs on it, so Ill take a guess here and say it needs to know which
model to use for unique validation, since it has to query the database to execute the validator.
Then we have the signup method:
155
This is actually fairly simple to understand. It calls an instance of the User model, then uses the User
model methods to create the user, as long as the input has passed validation.
So one thing to note about controllers is that they can reference many different models. So far the
site controller has used 3 different form models and the user model.
Anyway, lets return to the actionSignup method on the controller. Here it is again for reference:
public function actionSignup()
{
$model = new SignupForm();
if ($model->load(Yii::$app->request->post())) {
if ($user = $model->signup()) {
if (Yii::$app->getUser()->login($user)) {
return $this->goHome();
}
}
}
return $this->render('signup', [
'model' => $model,
]);
}
At this point, we are pretty familiar with how Yii 2 loads the post data. Then it runs the signup
method on the model, which will create the user. Then it finds an instance of the user and logs them
in. Finally, it returns them to the homepage.
156
So you can see the naming convention follows what we would expect.
Ok, back to the model, lets step through this its pretty simple actually:
<?php
namespace frontend\models;
use common\models\User;
use yii\base\Model;
/**
* Password reset request form
*/
157
],
];
}
The only attribute here is $email. Notice on the rules in the exist array we get targetClass, filter,
and message. This shows us how sophisticated validation can be. We also see the one constant I
left in place on the User model being referenced. So if in the future, we want to replace that, we
have to do it here as well as in the model. But wait a minute. We changed the attribute from status
to status_id, so we will have to change that here. Go ahead and make the change now.
That line should look like this now:
'filter' => ['status_id' => User::STATUS_ACTIVE],
We also need to make the attribute name change here. Go ahead and change status to status_id.
Should look like:
158
$user = User::findOne([
'status_id' => User::STATUS_ACTIVE,
'email' => $this->email,
]);
The sendemail method looks up the user by email address to see if they are active. If were good
there, we test to see if there is a valid token, and if not generate one. If we can save it, we send the
token in the email. Otherwise return false.
So back to the site controller and the actionRequestPasswordReset method. So after attempting to
post the data and validate, it tries to send the email, and if good, sets the flash message of success.
If it passed validation but for some reason the email could not be sent, it shows a flash message for
error.
If the data is not posted, it shows the form.
Ok, one more action on the site controller, actionResetPassword($token). This one requires the get
variable from the url for the token:
public function actionResetPassword($token)
{
try {
$model = new ResetPasswordForm($token);
} catch (InvalidParamException $e) {
throw new BadRequestHttpException($e->getMessage());
}
if ($model->load(Yii::$app->request->post())
&& $model->validate() && $model->resetPassword()) {
Yii::$app->getSession()->setFlash('success', 'New password was saved.');
return $this->goHome();
}
return $this->render('resetPassword', [
'model' => $model,
]);
}
No surprise, we have another form model. Now because we are expecting the token from the get
variable, we wrap the call to the model in a try catch block so that we can handle the error if we
dont get the expected token.
159
ResetPasswordForm Model
Lets look at the ResetPasswordForm model:
<?php
namespace frontend\models;
use
use
use
use
common\models\User;
yii\base\InvalidParamException;
yii\base\Model;
Yii;
/**
* Password reset form
*/
We see the namespace, use statements, class declaration and the class properties. Theres a comment
telling us we will reference the User model. Then we get a constructor that takes two arguments,
the token and a $config that defaults to an empty array. Im fairly certain the empty array is there
because of the parent constructor of Model, the class which is being extended.
160
/**
* Creates a form model given a token.
*
* @param string $token
* @param array $config name-value pairs that will be used to initialize
*the object properties
* @throws \yii\base\InvalidParamException if token is empty or not valid
*avoiding line-wrap in function. do not breakup lines in your code.
*/
Obviously again, we are avoiding wordwrap, so there are line breaks where there normally wouldnt
be. It makes the code look sloppy but there is nothing I can do about it. So lets just move on.
So two main things the constructor is doing. 1. It checks to see if the token is empty or not a string.
2. It does a User::findByPasswordResetToken($token) call to set the user to $_user.
If you remember when we went through the User model in detail, we saw the findByPasswordResetToken($token) method.
The constructor finishes by calling the parent.
Next we have the rules method:
161
This is fairly easy to get. The password will be set by the post data which the controller will feed to
the model. Then remove the token.
Ok, so to close out the controller actionResetPassword method:
if ($model->load(Yii::$app->request->post()) && $model->validate()
&& $model->resetPassword()) {
Yii::$app->getSession()->setFlash('success', 'New password was saved.');
return $this->goHome();
}
return $this->render('resetPassword', [
162
Post the data to the model, reset password and go home, or, show the view form.
Obviously Site Controller covered a lot of ground, but we still have a little more ground to cover.
That was the frontend.
Yii;
yii\filters\AccessControl;
yii\web\Controller;
common\models\LoginForm;
yii\filters\VerbFilter;
/**
* Site controller
*/
We are so used to this by now, we dont need to really comment here, other than to point out that
we will be using the same LoginForm model as the frontend did.
Next method, behaviors:
Then actionIndex:
163
164
165
Theres only one small change. Instead of calling login(), we call loginAdmin() from the LoginForm
model.
loginAdmin Method
We have not created that method, so lets go ahead and do so now. Insert the following method into
common/models/LoginForm.php.
Gist:
LoginAdmin
From book:
public function loginAdmin()
{
if (($this->validate())
&& PermissionHelpers::requireMinimumRole('Admin',
$this->getUser()->id)) {
return Yii::$app->user->login($this->getUser(),
$this->rememberMe ? 3600 * 24 * 30 : 0);
} else {
throw new NotFoundHttpException('You Shall Not Pass.');
166
}
}
Please look at the Gist if you want to see the proper formatting, that got really ugly because I had
to avoid wordwrap in the book.
Also add to the top of the file:
use yii\web\NotFoundHttpException;
use common\models\PermissionHelpers;
So what we have done here is added more to the if statement. Not only do we validate the user, but
we also use:
PermissionHelpers::requireMinimumRole(Admin, $this->getUser()->id)
This will ensure the user has a minimum role of admin. The reason we are handing in $this>getUser()->id is because the user is not yet logged in, so we need to tell it explicitly which user
we want to check. If you recall, we accounted for this scenario in the helper classes.
We dont need to check their status at this point because the getUser method calls findByUsername
and that method does it for us.
With one relatively minor change, we now control access to the admin login by requiring the user
to have at least a role of admin and an active status. And look how simple it was!
Our helper classes came in very handy and with those methods, we did not need verbose code to
get the values we wanted.
So now you can play around with this by registering users and setting their role_id via PhpMyadmin.
Make some that are User and some that are Admin and log in and out to both the frontend and the
backend. Its pretty cool.
Before we end the chapter, we can do one bit of cleanup. Lets discuss the constant on the user table:
const STATUS_ACTIVE = 1;
As I said in chapter four, leaving the constant in place violates DRY. But for ease of setup, I have left
it in place.
Now that we have our helper classes in place, we can replace it. Obviously this assumes you have
an Active status record in your status table.
If you wish to replace the constant, you can do this by finding every instance of:
167
Just substitute that for the constant in the appropriate places in the User model and the ResetPasswordRequestForm model and dont forget to include the use statements for ValueHelpers.
Then you can remove the constant entirely. If you remove the constant and you get an error, it means
you missed a reference to it in one of the models, so just go back over and find the missing reference.
Summary
Commit!
We covered a lot of ground by examining Site Controller in great detail. We learned a lot about
controllers, actions, views, and even more on form models. Now we know, having seen so many
examples, that the typical implementation of a form can involve a form view, a form model and a
controller.
We learned about the ActiveForm widget, which resides in the view and makes it easy for us to
render the form. We learned how to implement captcha and send flash messages from the controller
to the view.
We also learned about some of the more interesting actions, like resetPassword and their associated
form models. We saw how we can target the user class in the validation rules.
Finally, we got to implement a couple of changes that now control access to the backend by enforcing
a minimum value to the role of the user. We did that simply from using Helper methods from a
168
previous chapter, so that it seemed like it was a nothing change, and yet it was the beginning of our
building a control system throughout the application.
Weve taken care to explain everything in as much detail as possible. Hopefully enough of it is
sticking with you. If not, give it time, it will. Yii 2 requires patience and persistence to learn. Were
finally on our way to building the application. And so onward we march!
CRUD
CRUD stands for create, read, update and delete, a simple acronym to remember it. When you use
Gii to create CRUD, you get a controller, forms, and a search model. Its awesome!
So lets navigate back to Gii:
yii2build.com/index.php?r=gii
170
Gii CRUD
You can see that the filename conventions to follow. You need to create the search folder inside of
frontend/models before you use Gii to create the CRUD, so make sure the folder named search is
there before proceeding .
We already decided to put the Profile model in the frontend folder, so we will be doing the same for
the crud. Make sure the fields are filled in as follows:
Model Class is frontend\models\Profile
Search Class is frontend\models\search\ProfileSearch
Controller Class it is frontend\controllers\ProfileController
You can leave the view path blank if you are using defaults in the file structure, which is what we
are using.
We are providing Gii with a just little information and this time its going to create 8 files for us:
ProfileController.php
ProfileSearch.php
views/profile/_form.php
views/profile/_search.php
views/profile/create.php
171
views/profile/index.php
views/profile/update.php
views/profile/view.php
After you have created the search folder in frontend/models, go ahead and run the CRUD generator
for Profile. If you follow the preview/generate cycle on Gii, you should end up with those 8 files.
Lets briefly discuss what each file does.
Profile Controller
This is ProfileController.php and is found under frontend/controllers/ProfileController.php. We are
going to look at this in great detail in few minutes.
Profile Search
ProfileSearch.php is located in frontend/models/search/ProfileSearch.php and contains the logic that
powers the search form. This is basically a special extension of the base Profile model that is for
search. Because we wont be searching through profiles on the frontend, we wont be using it. Users
will only have one profile and therefore no need to search. I included it here because I wanted to
show you how to create this kind of file. We will use this kind of file for our backend later.
_search
The search form itself is a partial named _search.php that gets rendered to the Profile index page.
Since we are not allowing other users to search profiles, we will not be using this.
_form
_form.php is another partial view that contains the form that is rendered to the create and update
views. The same form partial is rendered into both create and update view pages.
Index
Index.php is the index view includes a ready made widget to display paginated record results in
organized columns, along with _search.php partial on top of the form. Although we wont need this
for frontend users, who only ever have one profile, we will need this for backend admin users who
can review the profiles of all users. Just to be perfectly clear, we will not be using this file at all, but
we will use one like it for the backend. We will leave it in place for now for demonstration purposes.
Once the file is made through Gii, you can reach this page at:
yii2build.com/index.php?r=profile/index
172
View
The view.php file is the details of an individual record and utilizes the DetailView widget. This page
will return a 400 error since we are not passing an id, which the controller is expecting. Just ignore
this for now, since we are going to change it anyway.
Create
Calling create.php renders the _form.php partial so you can input the needed data to create the
record.
Once the file is made through Gii, you can reach this page at:
yii2build.com/index.php?r=profile/create
Create Profile
Although this form would work if you formatted the input correctly, I dont recommend testing it
now. We are going to make deep changes to the controller and other changes to the form, so its
pointless to test it now.
173
Update
Obviously, this page also fails because it is expecting id as a parameter.
When you look at the create view page, you will notice that there are a lot of problems. Fields like
created_at show up as text fields and gender returns a number.
As I mentioned, if you try some of these urls without having created a profile, you will get errors.
You can create a profile by forcing your way through, but this is not what you want to do. We have
a lot of work to do before this UI is ready.
Also, because we havent stitched together the right navigation, the form doesnt know what user is
the right user to associate with the profile. Entering that manually exposes the question of security
and url manipulation, because even if you associate the right record manually, you have to prevent
users from hijacking records they dont own.
We need to do a bit of work on the controller and the views to get exactly what we need, the code
generation only goes so far.
The good news is that even in the state everything is in, Gii has provided us with much of what we
will need. It has also given us an easy-to-follow architecture that, which, with some modification
on our part, will create a robust template for user-owned records.
So, for whatever application requirements you have in the future, when it involves a user-owned
record, such as profile, preferences, blog entry, etc., you will have a working example that is easily
replicated. And once you get into the rhythm of this, you will be amazed at how it flows.
By starting with a user-owned record like profile, we are creating a structure that Gii does not hand
to us in perfect condition, thats the bad news. The good news is that when we build the backend
version of profile, to help admin manage profiles, it is a much closer fit to the boilerplate, so things
will become progressively easier as we go.
174
3. We will modify our main layout view so that we have navigation to the user profile.
When were done with this section, our Yii2build template will have taken a huge step forward.
Out of the box, users are able to register and login to the application. We have already extended
the Advanced Application Template to respect the difference between role level for frontend and
backend, so only users with a minimum role level of admin can log into the backend.
Tip
Just for the sake of clarity, I will mention that the frontend profile CRUD has nothing to
do with backend and will not appear there. We will build that separately when we create
the backend admin area.
By building our fully functional frontend profile model, users will be required to log in to build a
profile. The application will know whether or not they already have a profile, and if not, when they
click on the profile link, it will take them to the profile create page. The application will also enforce
rules to make sure the user is only able to access their own profile. We will also provide navigation
in the view pages that knows when it is appropriate to show the profile link.
So when were done with this part of the project, we will have tight template and an example to
follow if we want to create any other type of user-owned records that need to stay private to the
users. This is good stuff.
Yii;
frontend\models\Profile;
frontend\models\search\ProfileSearch;
yii\web\Controller;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
common\models\PermissionHelpers;
common\models\RecordHelpers;
175
This makes reference to our RecordHelpers class and our PermissionHelpers which we wrote in a
previous chapter.
Currently, the behaviors method only has a restriction on delete that says it must be done by the
post method:
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
];
}
We want to add some basic access control logic that restricts the user from the actions on this
controller unless they are logged in. So change behaviors to this:
Gist:
Controller Behaviors
From book:
public function behaviors()
{
return [
'access' => [
'class' => \yii\filters\AccessControl::className(),
'only' => ['index', 'view','create', 'update', 'delete'],
'rules' => [
[
'actions' => ['index', 'view','create', 'update', 'delete'],
'allow' => true,
'roles' => ['@'],
],
],
],
'verbs' => [
176
The @ symbol means logged in, so the listed actions can only be performed when the user is logged
in. Thats not really enough access control, but its start. We will do more later.
Please note that roles in this case does not refer to the role_id column on the user record, the two
have nothing to do with each other.
Index Action
Ok, now were ready for the actions. The index action, which is the default action of the controller,
looks like this:
/**
* Lists all Profile models.
* @return mixed
*/
This is meant to return a list of results and uses a different model, ProfileSearch, which extends the
profile model to provide search functionality. But in the case of our user profile, we only allow one
profile per user, so we wont be using this code.
We could just disable this action, cut it out of the controller completely, and delete the index.php
file, but some of the bread crumb navigation that is built into Yii 2 will then return a page not found
177
exception and that is not the behavior we want. Also, someone could type in r=profile into the url
and it would return the same page not found error, again behavior we do not want. We want all our
navigation super tight, so instead we will redirect Index.php to view.php.
View.php, if you recall, lists the details of the record, and this would be appropriate for someone
who wanted view their own profile.
Of course its not as simple as redirecting. We also have to apply the controlling logic that determines
whether the appropriate user has a profile, if so, show it to them, and if not, send them to the Create
page.
So lets replace the actionIndex method with the following:
Gist:
Index Action
From book:
public function actionIndex()
{
if ($already_exists = RecordHelpers::userHas('profile')) {
return $this->render('view', [
'model' => $this->findModel($already_exists),
]);
} else {
return $this->redirect(['create']);
}
}
That first line should look a little familiar to you. We talked about doing something like this when
we built the RecordHelpers method userHas. Now were ready to use it.
Just a quick reminder, the userHas method checks for a users record on the model supplied, so in
this case we are checking to see if the user has a profile record. If there is no record, it returns false,
if true, it returns the id of the record.
Lets describe exactly how this works. The first thing we are doing is calling the userHas method
from the RecordHelpers class and setting the result to $already_exists, wrapped in an if statement.
178
So if userHas evaluates true, it returns to the view file, with the correct model instance held in the
variable $already_exists.
It then uses the controllers findModel method to return that instance to the view, in this case its
named view.php, as it is passed along in the array:
'model' => $this->findModel($already_exists),
Now if $already_exists evaluates false, we redirect to the create view, since the user doesnt have a
profile, and they need to create one:
} else {
return $this->redirect(['create']);
}
By taking the extra effort to create the helper class, we extracted out some logic that keeps the
controller simple and clean. If we wanted to use Yiis relationship syntax in making the if condition,
we might write:
if($already_exists = Profile::find()->where(['user_id' => Yii::$app->user->identity>id])->one())
For the most part, its a trivial cosmetic difference, but we have the potential to use this syntax in
many places in the project. We can use it on other models simply by handing in a different model
name.
I mentioned in an earlier chapter about Robert C. Martins book called Clean Code. He makes the
point that programmers spend most of their time reading code, not writing it. So little syntactic
differences that make code easier to read, when scaled out over a large project, make a huge
difference in the speed and clarity of those who have to work on the system.
Another big change in the way we built our controller methods is that we eliminated the get variable
being handed into the method, so the record id cant be hijacked through the browser. This adds an
extra layer of security to the system. We will be adding more security later.
Now that we understand how RecordHelpers::userHas(profile) checks the association between the
user and the profile record, we are ready to step through the rest of the actions.
View Action
The view action:
179
/**
* Displays a single Profile model.
* @param string $id
* @return mixed
*/
Hey, thats exactly the same as the index action, whats up with that? Remember we said that we
were changing the default index action to be the same as the view action because we dont need a
list of profiles, only the correct profile for the user or the create form if they dont have one. So you
caught a break there and we just copied the code.
180
Create Action
Ok, on to the Create action. Heres what you get out of the box:
public function actionCreate()
{
$model = new Profile();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
} else {
return $this->render('create', [
'model' => $model,
]);
}
}
Basically this is saying if the form is loaded, save it and go to view or display the create form. Well,
we have a little more to our version of this:
Gist:
ActionCreate
From book:
public function actionCreate()
{
$model = new Profile;
$model->user_id = \Yii::$app->user->identity->id;
if ($already_exists = RecordHelpers::userHas('profile')) {
return $this->render('view', [
'model' => $this->findModel($already_exists),
]);
} elseif ($model->load(Yii::$app->request->post()) && $model->save()){
return $this->redirect(['view']);
181
} else {
return $this->render('create', [
'model' => $model,
]);
}
}
We start by calling a new instance of the model, then we set the user_id attribute of the model to
the current user via Yii::$app->user->identity->id. As Ive said before, the current user id is always
available to us via this static call of the Yii application class.
Next we check to see if the user already has a profile by running our RecordHelpers::userHas(profile)
method and setting the result to $already_exists.
if ($already_exists = RecordHelpers::userHas('profile')) {
return $this->render('view', [
'model' => $this->findModel($already_exists),
]);
}
If we get an model id in response, we show the view file of that id. We need to put this test on this
method because a user might be able to navigate directly to the create action, without going to index
or view first.
If $already_exists evaluates false, the next thing the code does is call the load method from the post
data and attempt to save it:
elseif ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view']);
}
But this will only happen if the load method has received data from the form that was posted. If it
receives it and can validate, it would save and then return the appropriate view page.
You might be wondering how it knows what user_id to assign in the newly created record, since
we are not setting it on the form, which means its not being passed in via post. It gets set on the
second line of the action method when we assign that $model->user_id to the current user. And
when $model calls the load method, it remembers this attribute.
Lastly, if there is no post data or if there are validation errors, we show the form:
else {
return $this->render('create', [
'model' => $model,
]);
}
Update Action
Next we move on to the actionUpdate method. Here is what Gii gave us:
/**
* Updates an existing Profile model.
* If update is successful, the browser will be redirected to the 'view' page.
* @param string $id
* @return mixed
*/
182
183
/**
* Updates an existing Profile model.
* If update is successful, the browser will be redirected to the 'view' page.
* @param string $id
* @return mixed
*if statement in two lines due to avoid wordwrap
*/
Profile::find()->where(['user_id'
=>
Yii::$app->user->identity->id])-
I did that because I felt that since the variable name was $model, it was a little more syntactically
logical to follow that with the model name we are interested in, instead of the helper class:
if($model = RecordHelpers::userHas('profile'))
the longer syntax just seems clearer that we are looking for a profile record that matches the current
user. Ok, so now we know why we are using:
184
if($model
>one())
Profile::find()->where(['user_id'
=>
Yii::$app->user->identity->id])-
So, lets break that down. Set the instance of the Profile model, where the user_id is the current user
to the variable $model, if you can. If it evaluates false, we jump down to the last else statement and
throw:
throw new NotFoundHttpException('No Such Profile.');
Otherwise, if we get our $model variable set with the correct instance of the model, we call the data
from the post form and try to save it via the next if statement.
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view']);
}
If the update is successful, how does it know which view to display? The redirect method, which in
this case is only supplied with an action, is not rendering. It is routing it to the named action of the
current controller, so in this case, the view action will determine the right view to display.
Of course, this also assumes there is post data.
If there is no post data, no change from what is there already, it will return to the form, with our
model handed in so it can pre-populate correctly.
else {
return $this->render('update', [
'model' => $model,
]);
}
Once again, no get variables were used, so there will be no hijacking the records from get variables.
Delete Action
Our final change to the ProfileController will be on the delete action. We got this from Gii:
185
Profile::find()->where([
'user_id' => Yii::$app->user->identity->id
])->one();
$this->findModel($model->id)->delete();
return $this->redirect(['site/index']);
}
Here we set the $model using same method from update for the same reasons, since we dont have
a get variable. Then we use the delete method, handing in the $model->id.
Then we redirect to the site index page. If we just put index as the value, the controller would
assume it was profile/index, which is not what we want.
FindModel Action
The last method in the Profile controller, we are not changing:
186
This is the method to find a particular instance of the model. You hand in the $id you are looking
for and Yii returns that instance of the model, a very useful method indeed.
One thing we can appreciate about Yii 2 is the controller code is very clean, clear and concise. All
the heavy lifting is abstracted out and we are simply providing a small set of instructions for the
actions to follow. This is modern PHP at its best.
View.php
Ok, lets start with view.php. Here is what we got from Gii:
<?php
use yii\helpers\Html;
use yii\widgets\DetailView;
/* @var $this yii\web\View */
/* @var $model frontend\models\Profile */
$this->title = $model->id;
$this->params['breadcrumbs'][] = ['label' => 'Profiles', 'url' => ['index']];
187
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="profile-view">
<h1><?= Html::encode($this->title) ?> Profile</h1>
<p>
<?= Html::a('Update', ['update', 'id' => $model->id],
['class' => 'btn btn-primary']) ?>
<?= Html::a('Delete', ['delete', 'id' => $model->id], [
'class' => 'btn btn-danger',
'data' => [
'confirm' => 'Are you sure you want to delete this item?',
'method' => 'post',
],
]) ?>
</p>
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'user_id',
'first_name:ntext',
'last_name:ntext',
'birthdate',
'gender_id',
'created_at',
'updated_at',
],
]) ?>
</div>
This is going to be our first in-depth look at the DetailView widget, which we will see in a minute.
Next we have some comments from Yii, $this is the view model, and $model is the profile model
thats been handed in through the controller, so it represents the exact instance of the profile model
that we need.
188
You can always refer to those comments if you are confused about which variables represent which
models.
Next we set the title with $model->id:
$this->title = $model->id;
$this->params comes from the view base class. When you render a view, in this case, view.php, you
are calling an instance of yii\webView, which extends the yii\base\ view class, and through the magic
of Yii 2s routing, making the object available as $this, hence the comment:
/* @var $this yii\web\View */
Its a very powerful architecture. You also hand in your model or models via the controller as well,
so you have a lot of capabilities for manipulating data in the views. Yii 2 does all of this, and at the
same time, makes it look simple, keeping as much PHP coding and logic out of the view as possible.
Ok, moving on. When we set the div, the h1, and some nav:
<div class="profile-view">
<h1><?= Html::encode($this->title) ?> Profile</h1>
<p>
<?= Html::a('Update', ['update', 'id' => $model->id],
['class' => 'btn btn-primary']) ?>
<?= Html::a('Delete', ['delete', 'id' => $model->id], [
'class' => 'btn btn-danger',
'data' => [
'confirm' => 'Are you sure you want to delete this item?',
'method' => 'post',
],
]) ?>
</p>
The button style on Delete is btn btn-danger and you hand in the additional data parameters of
confirm and method. This results in a confirmation alert, pretty handy for delete functionality and
it makes for great UI, right out of the box.
Finally we have the detailview widgets:
189
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'user_id',
'first_name:ntext',
'last_name:ntext',
'birthdate',
'gender_id',
'created_at',
'updated_at',
],
]) ?>
</div>
You can easily add or subtract attributes and well show you by example. Lets go ahead and replace
the entire view file with:
Gist:
Profile View
From book:
<?php
use yii\helpers\Html;
use yii\widgets\DetailView;
use common\models\PermissionHelpers;
/**
* @var yii\web\View $this
* @var frontend\models\Profile $model
*/
$this->title = $model->user->username . "'s Profile";
$this->params['breadcrumbs'][] = ['label' => 'Profiles', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="profile-view">
<h1><?= Html::encode($this->title) ?></h1>
190
<p>
<?Php
//this is not necessary but in here as example
if (PermissionHelpers::userMustBeOwner('profile', $model->id)) {
echo Html::a('Update', ['update', 'id' => $model->id],
['class' => 'btn btn-primary']);
} ?>
<?= Html::a('Delete', ['delete', 'id' => $model->id], [
'class' => 'btn btn-danger',
'data' => [
'confirm' => Yii::t('app', 'Are you sure to delete this item?'),
'method' => 'post',
],
]) ?>
</p>
<?= DetailView::widget([
'model' => $model,
'attributes' => [
//'id',
'user.username',
'first_name',
'last_name',
'birthdate',
'gender.gender_name',
'created_at',
'updated_at',
//'user_id',
],
]) ?>
</div>
Now lets step through the changes. To start, we made some cosmetic changes to the title to display
username, which are trivial, and we pulled in an additional use statement:
191
use common\models\PermissionHelpers;
This will give us access to the PermissionHelpers::userMustBeOwner() method. This is one of the
helper methods we created back in the Helpers chapter, which returns true or false to determine if
the current user is the owner of the record.
We are going to use userMustBeOwner(), to put an extra layer of security on our navigation in
frontend/views/profile/view.php. In this case, we wrap the method in an if statement, and if true,
we display the navigation:
<p>
<?Php
//this is not necessary but in here as example
if(PermissionHelpers::userMustBeOwner( 'profile', $model->id)) {
echo Html::a('Update', ['update', 'id' => $model->id],
['class' => 'btn btn-primary']);
} ?>
The method userMustBeOwner() takes 2 arguments, first, the name of the model handed in as a
string, second, the id of the model instance, available to us as $model->id because we sent the model
instance to the view through the controller.
As the comment in the code indicates, this test on the user is not really necessary because for
profile/view, you cant get to the view page without being the owner of the record and the update
action also tests to limit access to the owner only.
I included this here simply as an example for cases where you might have view records visible to
all users, but actions like update only available to record owners and in those cases, you would only
want the link to update visible to record owners. A blog where authors can update their posts, but
other users can only read them would be an example of this.
I could have done the same test on the delete link, but again, not necessary, since the controller tests
for the owner and only record owners get to be on the view page where this nav resides.
Finally, we have some changes to the DetailView widget:
192
<?= DetailView::widget([
'model' => $model,
'attributes' => [
//'id',
'user.username',
'first_name',
'last_name',
'birthdate',
'gender.gender_name',
'created_at',
'updated_at',
//'user_id',
],
]) ?>
We commented out the id and user_id fields because these numbers will not mean anything to
end users. Instead we popped in user.username and gender.gender_name. We are accessing these
properties through lazy-loaded relationships and that is the syntax for that. Pretty simple right?
Touching briefly on what lazy load means, it means that there will be a query for each row, in this
case 2 separate queries. Thats probably fine on a page like this that has a small number of queries.
For large lists with multiple queries, it would highly inefficient and we need to use eager loading in
those cases, and we will show you how to do that when we are dealing with those kinds of results.
We need to do a little housework on the Gender model in regards to attribute labels, so it displays
what we want correctly on the page.
Gender
So lets start with the simple one, Gender.php located in frontend/models.
We have one change only to the attribute labels. I wont provide a Gist, since its a one word change:
public function attributeLabels()
{
return [
'id' => 'ID',
'gender_name' => 'Gender Name',
];
}
193
Thats the one attribute label I left to work on when we built the new models in the New Models
chapter. All the other ones, we already have in place, as well as all the additional relationships.
You can see we put those other labels together outside of normal workflow, otherwise we would be
bouncing around between models and views. I thought it was better to keep a tighter focus on one
thing at a time, so people who are new to the framework, will be able to absorb the information
easier.
Form Partial
Ok, next were going to modify the _form.php, which is a partial. A partial, which in Yii 2 is
designated by the underscore in front of the filename, is a view gets included into another view,
in this case via:
<?= $this->render('_form', [ 'model' => $model, ])?>
Through the magic of Yii 2s routing and file structure, it knows which _form you are referring to.
This makes for very concise code. Lets take a look at how we use it.
The above code is called in Update.php, for example. This is a perfect time to mention that the
_form.php is simpler than the contact one we looked at earlier.
For example, in this case, since we are just doing straight CRUD actions, we dont need a separate
form model. We dont even need to specify a form model at all because once again Yii 2 knows from
the file structure, and from the model being handed into the view from the controller, which model
to update. This is very cool and saves you a lot of time.
So for straight creating, updating, deleting, you typically dont need a separate form model. Its only
when there is some complicated validation or other processes will you need a form model.
With our example here, Update.php, $model is the Profile model.
Ok, lets get back to the _form. This is what Gii gave us on boilerplate:
194
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/* @var $this yii\web\View */
/* @var $model frontend\models\Profile */
/* @var $form yii\widgets\ActiveForm */
?>
<div class="profile-form">
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'user_id')->textInput(['maxlength' => 11]) ?>
<?= $form->field($model, 'first_name')->textarea(['rows' => 6]) ?>
<?= $form->field($model, 'last_name')->textarea(['rows' => 6]) ?>
<?= $form->field($model, 'birthdate')->textInput() ?>
<?= $form->field($model, 'gender_id')->textInput(['maxlength' => 10]) ?>
<?= $form->field($model, 'created_at')->textInput() ?>
<?= $form->field($model, 'updated_at')->textInput() ?>
<div class="form-group">
<?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update', ['class' =>
$model->isNewRecord ? 'btn btn-success' : 'btn btn-primary'])
?>
</div>
<?php ActiveForm::end(); ?>
</div>
This is nice concise code and you have to love the framework for that. But there are some things
we dont need. The user id does not need to be displayed in the form, it is set on the model in the
controller, so it gets saved correctly without form input. We can also delete the fields for created_at
and updated_at as we have added behaviors on the Profile model which automatically insert those
for us.
195
Then we have just 2 other changes, we will put a note under the date field to explain the input and
create a dropdown list for Gender. Here is what the entire file should look like:
Gist:
Form Partial
From book:
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/**
* @var yii\web\View $this
* @var frontend\models\Profile $model
* @var yii\widgets\ActiveForm $form
*/
?>
<div class="profile-form">
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'first_name')->textInput(['maxlength' => 45]) ?>
<?= $form->field($model, 'last_name')->textInput(['maxlength' => 45]) ?>
<br/>
<?= $form->field($model, 'birthdate')->textInput() ?>
* please use YYYY-MM-DD format
<br/>
<?= $form->field($model, 'gender_id')->dropDownList($model->genderList,
['prompt' => 'Please Choose One' ]);?>
<div class="form-group">
<?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update',
['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end(); ?>
196
</div>
Note that <?= is the short statement for <?php echo. Each form field gets a separate line and you can
see how we did it above.
Going back to _form, we should pay attention to the field for gender_id:
<?= $form->field($model, 'gender_id')->dropDownList($model->genderList, [ 'prompt' =>
'Please Choose One' ]);?>
Two things to note. We inserted the dropDownList method using $model->genderList, which is using
a magic get, hence the lowercase g in gender. We can do this because of the relationship method
getGenderList that we added to Profile in a previous chapter.
We also added in a parameter inside of an array for the prompt because we dont want the list to
default to the first value, which is what it would do if the prompt were not there.
Now onto the small remaining view changes that we have in mind for the frontend Profile views.
Create
Open create.php:
and change this line:
$this->params['breadcrumbs'][] = ['label' => 'Profiles', 'url' => ['index']];
to this line:
$this->params['breadcrumbs'][] = ['label' => 'Profile', 'url' => ['index']];
Update
Now onto update.php. Chop out the 2nd breadcrumb line and change the title, so it looks like this:
197
<?php
use yii\helpers\Html;
/* @var $this yii\web\View */
/* @var $model frontend\models\Profile */
$this->title = 'Update '. $model->user->username . "'s Profile ";
$this->params['breadcrumbs'][] = ['label' => 'Profile', 'url' => ['index']];
$this->params['breadcrumbs'][] = 'Update';
?>
<div class="profile-update">
<h1><?= Html::encode($this->title) ?></h1>
<?= $this->render('_form', [
'model' => $model,
]) ?>
</div>
And just for consistency, lets take that s out of the word Profile on the view.php breadcrumbs as
well:
$this->params['breadcrumbs'][] = ['label' => 'Profile', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
That wraps up the changes for the Profile views. Youll notice that we didnt update index.php or
_search.php. We dont need those files, they were auto-generated by Gii. Instead of getting rid of
them now, we will wait until later in the project to delete because if we change our minds and want
to use them, we have them and dont have to recreate. If your sense of workflow is offended by this,
then feel free to delete them now.
Site Layout
Its time we modified the site layout to include a link to Profile in the header. We want this link to
appear next to the logout link, which is what appears when you are logged in. So we need to modify
frontend/views/layout/main.php
198
views\layouts\main.php
This is where the file is located. There is a similar one in the backend, so make sure you are in the
right place.
This is what you get out of the box with the advanced template:
<?php
use yii\helpers\Html;
use yii\bootstrap\Nav;
use yii\bootstrap\NavBar;
use yii\widgets\Breadcrumbs;
use frontend\assets\AppAsset;
use frontend\widgets\Alert;
/* @var $this \yii\web\View */
/* @var $content string */
AppAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
<meta charset="<?= Yii::$app->charset ?>"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<?= Html::csrfMetaTags() ?>
<title><?= Html::encode($this->title) ?></title>
199
200
</div>
<footer class="footer">
<div class="container">
<p class="pull-left">© My Company <?= date('Y') ?></p>
<p class="pull-right"><?= Yii::powered() ?></p>
</div>
</footer>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>
This is a nice concise template that does quite a bit. The AppAsset::register($this);pulls in the
style sheets and js. for the template. Yii 2 utilizes a publishing system to cache assets. You have to
pay careful attention to your config files.
Later in the book, we will cover implementing a new asset, but we wont go too deep. This book
doesnt really cover fronted decoration in great detail.
But we can spend a minute on describing how layout works. If you look in the body of the file, youll
see:
<?= $content ?>
That rather inconspicuous statement places the view inside the layout. Yii 2s routing mechanisms
know which view and which layout to use. You can use multiple layouts and themes and this subject
gets deep quickly, but like I said, were only covering the surface. You can see the section near the
bottom is:
<footer class="footer">
201
Profile Link
So the next thing we are going to do is add a link to Profile. Now, the way we setup the Profile
controller is that the index action first tests to see if a record exists. And if so, it redirects to the
view page and if not, redirects to the create view. So we really only need one link to Profile and it
covers everything. The update and delete views are already linked from within the view.php file, so
no need to create that nav.
What we do want to test for however, is that the user is logged in. We do not want to show the profile
link if the user is not logged in. Also note, we do not need to provide a get variable. We eliminated
the need for that with the way we wrote the controller logic.
Insert the following:
$menuItems[] = ['label' => 'Profile', 'url' => ['/profile/view']];
Put this in the else statement between the opening and closing NavBar:
if (Yii::$app->user->isGuest) {
$menuItems[] = ['label' => 'Signup', 'url' => ['/site/signup']];
$menuItems[] = ['label' => 'Login', 'url' => ['/site/login']];
} else {
$menuItems[] = ['label' => 'Profile', 'url' => ['/profile/view']];
$menuItems[] = [
label' => 'Logout (' . Yii::$app->user->identity->username . ')',
'url' => ['/site/logout'],
'linkOptions' => ['data-method' => 'post']
];
}
So were using Yii::$app->isGuest method to determine whether or not the user is logged in, then
if so, we show them the profile link. The $menuItems array format works because it is in between
NavBar::begin and NavBar::end.
Ok, so with all these changes in place, we can login as a user and play with creating a profile,
updating, deleting, etc. Go ahead and make sure everything is working properly.
Try appending an invalid get variable to the url, such as:
http://yii2build.com/index.php?r=profile/update&id=7
Youll see that no matter you put into that id, it will return only the current users update link. So
at this point, this should all be working as expected.
If not, retrace your steps and try to find the typo.
Now before we close out the chapter, lets get rid of the ugly date input on the _form partial.
202
DatePicker
Lets make sure we remember to include the use statement on _form.php:
use yii\jui\DatePicker;
This is a 3rd party library that is not included in the base install of Yii. When we updated composer
to make sure we had Gii, we were supposed to also install the jui library. You should have:
"yiisoft/yii2-jui": "*"
You can see the last line we added yiisoft/yii2-jui: * Dont forget the comma on the previous line.
If that was not there, then you dont have it, so you need to add that line now. Then go to your
command line and type in:
\var\www\yii2build>composer update
This will bring in the yii2-jui dependencies. So now we are set to use it.
The next thing is to replace the form input line in _form.php with:
<?php echo $form->field($model,'birthdate')->widget(DatePicker::className(),
['clientOptions' => ['dateFormat' => 'yy-mm-dd']]); ?>
Go ahead and make the change and refresh your page. If all went well, you should now see a
difference on your update and create form from your Profile.
Please note that is not going to work correctly. I wrote that from an earlier version of the widget,
Yii 2.0 and the configuration is now a bit different, since we are working in Yii 2.0.3 or above.
Having to solve the problem the old config created is instructive, so for the moment, Im going to
pretend like the solution doesnt exist and we need to come up with a fix. Its not that uncommon
for you to face such issues and this is a simple scenario to work with and learn how to overcome it.
The problem here boiled down to the fact the widget was not respecting the date format and the
default format it uses would not pass validation. So essentially, update and create are dead until we
fix it.
203
So if the birthdate attribute is not empty, take the $this instance of birthdate and format it using:
date('Y-m-d', strtotime($this->birthdate))
Those are two built-in PHP functions there, date and strtotime. Anyway, we set $this->birthdate to
the $new_date_format, which is now holding the birthdate that was handed in, in the correct date
format.
Then we return Parent::beforeValidate(); to insure the method gets called and thats it.
Ok, so you can see how to work around a formatting issue like that.
A reader was kind enough to supply the newer config for the widget, which takes care of the problem,
and he also added a cool option to make the year easier to set.
Gist:
DatePicker Solution
From book:
204
Please note that when you use the dropdown to select year, you must also select the day or it wont
be set. Also, dont forget to remove the beforeValidate method from Profile.php if you added it there,
it was just for demonstration and is not necessary.
Summary
Commit!
So there you have it, a working user profile. You can just click on the link now and test all the pages.
You can see we finally made the leap into development, no longer confined to just learning about
how things work.
You saw that we got to use our helper class to help us test whether or not a user has a profile. We
built our helpers in advance because I covered the concept as a whole outside of workflow. When
you are developing an application, you will think of things like that as you go, which is perfectly
fine.
You also got to see a large number of efficiencies handed to us by Yii 2. We got to use the Gii
tool, which handed us a ready-made architecture that only needed a little tweaking. The controller
needed the most work, but only because these are private, user-owned records and the template is
geared towards public records. When we do the backend, the Gii output will more closely match
what we want.
205
And finally we modified our views, making the application more intuitive. And right away we feel
the difference. Its starting to come together.
Lets put our new controller in the frontend because our frontend users will be the ones who need
to upgrade. Put this in the field for Controller Class:
frontend\controllers\UpgradeController
Whether you are creating a frontend or backend controller, they will follow the above convention,
just use the appropriate starting folder.
Make sure the following fields are set if they do not auto-populate:
Action Ids: index
Leave the view path empty, it will know what the correct path is because we are using the default
setup.
It should look like this:
207
Ok, lets generate the code. This will not only create the controller, but also the corresponding view
folder and file. In this case we only have one action we want to create, which is index. You can do
more than one action by separating them with commas. In this case, the index action will render
the index view, which you would use to offer the user payment options.
Were only going to be mocking up the payment options page, well just put a little content on our
index view, thats as far as our instructions will go for this book. If you actually want to implement
real payment options, I would recommend checking out Stripe. For small companies, this solution
makes sense, and they have a lot of documentation for integration:
Stripe
Also, there is:
PayPal
You may want to offer both options.
Upgrade Controller
Ok, lets go back to Gii. Once you run generate on Gii, you will get:
UpgradeController.php
208
<?php
namespace frontend\controllers;
class UpgradeController extends \yii\web\Controller
{
public function actionIndex()
{
return $this->render('index');
}
}
Upgrade View
Pretty simple, it just renders the view, which is in views/upgrade/index.php:
<?php
/* @var $this yii\web\View */
?>
<h1>upgrade/index</h1>
<p>
You may change the content of this page by modifying
the file <code><?= __FILE__; ?></code>.
</p>
Again, nothing to it. So since were just mocking up, we could leave it at that. Now to test how this
works, we can simply add one line in our ProfileController.php file.
Require Upgrade To
Ok, now that weve jumped back to our Profile Controller in frontend/controllers/ProfileController.php, lets make it the first line of the actionUpdate:
PermissionHelpers::requireUpgradeTo('Paid');
209
So we made no change except for adding the first line, which now tells the controller that the user
has to be a user_type_name Paid. Make sure you save the change, then you can test this if you
login with an existing user that only has the default user_type of Free.
Tip
You should register several users through the application and then play around with their
user_type_id settings in PhpMyAdmin, so you can try different scenarios.
Now that you have a test user that is a Free user_type, you can try to update your profile, and
when you do, it will redirect you to the upgrade view. How simple is that?
Access Control
While were here in the Profile controller, we can talk a little more about access control. One of
the more not-so-obvious ideas would be to require the user to have an Active status to access the
Profile controller. Now of course the application requires it for login, so no one gets to login if they
do not have active status. But what if someone downgrades their status, then hits the back button?
Theoretically they can still access everything and this is sloppy control over the content.
210
So we going to add an access rule that requires a status of active. Dont worry, this will be really
simple because we already anticipated this and built the helper for it. And also, Yii 2s behaviors
class includes access control, which weve already seen in action. Now were just going to add a
little more to it:
Change the existing behaviors method in the Profile controller to the following:
Gist:
Profile Controller Behaviors
From book:
public function behaviors()
{
return [
'access' => [
'class' => \yii\filters\AccessControl::className(),
'only' => ['index', 'view','create', 'update', 'delete'],
'rules' => [
[
'actions' => ['index', 'view','create', 'update', 'delete'],
'allow' => true,
'roles' => ['@'],
],
],
],
'access2' => [
'class' => \yii\filters\AccessControl::className(),
'only' => ['index', 'view','create', 'update', 'delete'],
'rules' => [
[
'actions' => ['index', 'view','create', 'update', 'delete'],
'allow' => true,
'roles' => ['@'],
'matchCallback' => function ($rule, $action) {
return PermissionHelpers::requireStatus('Active');
}
],
],
],
211
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
];
}
If you get an error on not finding the VerbFilter class, make sure to have this use statement at the
top of the file:
use yii\filters\VerbFilter;
This was a class path that got changed by the Yii 2 Framework itself during the course of time when I
was writing the book, so if I missed that anywhere else, that is the fix for it. When you are developing
an application, its easy to miss a use statement. The good news is that Yii 2 will complain nicely
and point right to the missing file, you just need to format the use statement properly.
The complete use statements should look like this:
use
use
use
use
use
use
use
use
Yii;
frontend\models\Profile;
frontend\models\search\ProfileSearch;
yii\web\Controller;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
common\models\PermissionHelpers;
common\models\RecordHelpers;
I included the one for ProfileSearch in case you want to play around with it. Feel free to eliminate
any use statement that is not necessary to the controller.
Anyway, back to the behaviors. You can see that we added another array labeled access2:
212
'access2' => [
'class' => \yii\filters\AccessControl::className(),
'only' => ['index', 'view','create', 'update', 'delete'],
'rules' => [
[
'actions' => ['index', 'view','create', 'update', 'delete'],
'allow' => true,
'roles' => ['@'],
'matchCallback' => function ($rule, $action) {
return PermissionHelpers::requireStatus('Active');
}
],
],
],
Now the key of the array, access2, is just a string and you can call it anything you want. So you can
see what we did here. We copied the access array and added the matchCallback element, which is
a php callable that is looking for true or false.
Fortunately, we have a ready-made PermissionHelpers method that returns true or false, in this case,
checking to see if the current user has a status of active. If not, it does not allow access, and since
we have set the rule to all actions, they cannot access anything that the Profile Controller controls.
Its all done very simply.
Now I have created it this way because I want to demonstrate that you can have multiple layers of
access rules. But in reality, since the matchCallback applies to all actions, as does the other rules,
you could have simply added matchCallback to the first array, instead of creating access2.
You can also nest arrays under rules when you have rules that only apply to certain actions, for
example:
return [
'access' => [
'class' => \yii\filters\AccessControl::className(),
'only' => ['index', 'view','create', 'update', 'delete'],
'rules' => [
[
'actions' =>['create', 'update', 'delete'],
'allow' => true,
'roles' => ['@'],
],
213
],
'rules' => [
[
'actions' => ['index', 'view','create', 'update', 'delete'],
'allow' => true,
'roles' => ['@'],
'matchCallback' => function ($rule, $action) {
return PermissionHelpers::requireStatus('Active');
}
],
],
],
],
The access control class will iterate for each set of rules. So this is a very flexible and easy to use
method of access control.
You can see that when we simply want to restrict access, we can use behaviors. When we need to
restrict based on a condition and redirect or do some other action, we can build a helper that will
keep our controller code very clean.
Lets return now to the Upgrade Controller. This time we will use the more concise version of
behaviors, for cleaner code:
Gist:
Upgrade Behaviors
From book:
public function behaviors()
{
return [
'access' => [
'class' => \yii\filters\AccessControl::className(),
'only' => ['index'],
'rules' => [
[
'actions' => ['index'],
214
Now we will have to add some use statements, when its complete it should look like this,
Gist:
Use Statements
From book:
use
use
use
use
use
use
use
Yii;
yii\web\Controller;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
common\models\PermissionHelpers;
common\models\RecordHelpers;
frontend\models\Profile;
215
So you can see Ive simply set $name to the username of the current user. Im doing this to show
you how easy it is to move variables and objects into the view in the render method. You just add
the array with the name of the element and the variable as value, like so:
return $this->render('index',['name' => $name]);
If you have more than one object or variable, you can separate them by commas in the same arrray.
So now that we have the username available to us, lets just test it in the view. Replace index.php
with:
Gist:
Upgrade Index
From book:
<?php
/* @var $this yii\web\View */
?>
<h1>Hey "<?php echo $name; ?>,"
<p>
You can get the access you want by upgrading, but <?php echo $name; ?>,
that's not all. You get to go everywhere, isn't that cool?
</p>
This is just goofy fun, but you get the point. So thats just a simple variable, lets try an object:
Gist:
Upgrade Index 2
From book:
216
Here we are setting $name to an instance of Profile, where the user_id is the current user. To get this
to work, we will have to make sure we have a use statement to access Profile:
use frontend\models\Profile;
<p>
You can get the access you want by upgrading, but
<?php echo $name->first_name; ?> , that's not all.
You get to go everywhere, isn't that cool?
</p>
So now we have personalized the output to the users first name. You can have all kinds of fun with
this, but the point is that it is extremely simple to bring in an object and access it. Make sure you
are logged in when you try this or it wont work.
Later, when we code the backend, we will return objects holding lists of users and profiles, using Yii
2s built-in iterator and widgets.
217
Summary
Commit!
In this chapter, we took a little more control over our application. We created an upgrade
controller and view and then enforced rules to bring the user to the page if they didnt meet
the minimum user type allowed for access. We did this by adding a PermissionsHelper method,
requireUpgradeTo(Paid) to the update method on the profile controller.
We also added a requirement for the user to have a status of active. This tightens up security and
access and did this by adding a matchCallback requirement to the access rules in the behaviors
method. These were very simple changes that didnt bloat or confuse the code, in part due to the
fact that we extracted out logic to our helpers.
Finally, we played around with moving variables and objects from the controller to the view, to give
us an idea of easy it is to work with the data that is accessible to us.
It took a while for us to get to a point where we could utilize the models we built and have a little
fun with it, but at least by now you should be starting to get a sense of what development in Yii 2 is
like.
Index
Open up frontend/views/site/index.php. We will start by adding a couple of use statements:
Gist:
Use Statements Site Index
From book:
use
use
use
use
use
\yii\bootstrap\Modal;
kartik\social\FacebookPlugin;
\yii\bootstrap\Collapse;
\yii\bootstrap\Alert;
yii\helpers\Html;
Those go at the top of the file under the opening Php tag.
If you have not already imported the Kartik social extension, we need to do it now. Check for the
following in your composer.json file:
219
"minimum-stability": "stable",
"require": {
"php": ">=5.4.0",
"yiisoft/yii2": "*",
"yiisoft/yii2-bootstrap": "*",
"yiisoft/yii2-swiftmailer": "*",
"kartik-v/yii2-social": "dev-master",
"fortawesome/font-awesome": "4.2.0"
},
You can see it on the 2nd to last line. If you dont by this point have the font-awesome line in your
composer.json, you should add that too, we will need it later. Then run composer update from the
command line, like we have done many times before.
Composer Update
That should import the extension, if you did not have it already. You can check under your vendor
directory for a folder named Kartik-v, which is the folder for the extension.
Tip
You can find many useful extensions by Kartik at his site, Krajee.com. He is a superstar
developer. As of this writing, Kartik has 28 extensions/goodies that cover everything from
the social widget were using here to GridView extensions and more. If you do use his
extensions, be kind and donate if you can. Donations keep him working on new things to
add to the framework and that helps everyone.
Facebook Widget
Next, lets go back to the Site view file Index.php and change the title to:
$this->title = 'Yii 2 Build';
220
<div class="site-index">
<div class="jumbotron">
<?php if (Yii::$app->user->isGuest) {
echo Html::a('Get Started Today', ['site/signup'],
['class' => 'btn btn-lg btn-success']);} ?>
</p>
<h1>Yii 2 Build</h1>
<p class="lead">Use this Yii 2 Template to start Projects.</p>
<br/>
<?php
echo FacebookPlugin::widget(['type'=>FacebookPlugin::LIKE,
'settings' => []]); ?>
</div>
Now if you save that and hit refresh, you will get the following error:
Invalid Configuration yii\base\InvalidConfigException
The Facebook 'appId' has not been set.
221
Facebook Nav
Once you are on your settings page, you can get to the footer and the developers link:
Facebook Footer
Facebook Platform
222
Facebook Select
Facebook Confirm
223
At the bottom of the quick start screen, fill in your site url. Please note that Im using Yii2build.com
and you will not be able to use that. In the example implementation that we see later, make sure you
use your domain and not Yii2build.com. Your domain does not need to be a live site, so go ahead
and enter your domain:
We get a SDK setup finished (but we are not done, click on the Skip to Developer Dashboard link
under Next Steps).
224
That will bring you to the Dashboard, where you click on settings:
225
Obviously, I scratched out my app id. Yours will appear there. In order to copy the app secret, select
the show button.
You will need to copy the app id and the app secret into the appropriate area in config as I describe
below, but dont try to do it yet.
// the global settings for the facebook plugins widget
'facebook' => [
'appId' => 'your id',
'secret' => 'your secret',
],
We havent added that yet, but we will in a moment. Also, remember to provide a working email
address inside the Facebook settings page, otherwise the app wont work.
226
Facebook Configuration
Ok, on to setting up our common config settings to recognize Kartiks social module, which will
allow us to use his social widgets. If you recall, we included:
"kartik-v/yii2-social": "dev-master",
in our composer.json file. Now we need to tell config that it is there. The original common/config/main.php is:
<?php
return [
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'components' => [
'cache' => [
'class' => 'yii\caching\FileCache',
],
],
];
If you are not seeing that, make sure you are in the rigth file. Yii2 has several files named main.php
in config. You need the one that is in the common folder. You need to change this to:
Gist:
Common/Config/Main Update
From book:
<?php
return [
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'extensions' => require(__DIR__ . '/../../vendor/yiisoft/extensions.php'),
'modules' => [
'social' => [
// the module class
'class' => 'kartik\social\Module',
// the global settings for the disqus widget
'disqus' => [
'settings' => ['shortname' => 'DISQUS_SHORTNAME'] // default settings
],
// the global settings for the facebook plugins widget
'facebook' => [
'appId' => 'your id',
'secret' => your 'secret',
],
// the global settings for the google plugins widget
'google' => [
'clientId' => 'GOOGLE_API_CLIENT_ID',
'pageId' => 'GOOGLE_PLUS_PAGE_ID',
'profileId' => 'GOOGLE_PLUS_PROFILE_ID',
],
// the global settings for the google analytic plugin widget
'googleAnalytics' => [
'id' => 'TRACKING_ID',
'domain' => 'TRACKING_DOMAIN',
],
// the global settings for the twitter plugin widget
'twitter' => [
'screenName' => 'TWITTER_SCREEN_NAME'
],
],
// your other modules
],
'components' => [
'cache' => [
'class' => 'yii\caching\FileCache',
],
],
];
Take your Facebook appId and your secret and copy them into:
227
228
Please note that when you see the facebook widgets implemented in my examples, I use Yii2Build.com
as my domain. Obviously you will use your own domain example.
Extensions
Just a note about how Kartiks social module is referenced in config. Under vendor/yiisoft is a file
named extensions.php and this holds the alias for the module:
'kartik-v/yii2-social' =>
array (
'name' => 'kartik-v/yii2-social',
'version' => '9999999-dev',
'alias' =>
array (
'@kartik' => $vendorDir . '/kartik-v/yii2-social',
),
),
You can see kartik-v/yii2-social is the same name as it has in composer.json when we included it
there. Composer automatically entered it into the extensions file for us. Also note the array for alias
@kartik $vendorDir . /kartik-v/yii2-social , which allows for this line in config:
return [
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'extensions' => require(__DIR__ . '/../../vendor/yiisoft/extensions.php'),
'modules' => [
'social' => [
// the module class
'class' => 'kartik\social\Module',
I included the vendor path for reference, but thats not new, we did not add that, it was already
there. We did tell it to use extensions in the path specified in the config and that is where it finds
the alias to connect everything.
229
So basically, it says for the social module, use the class kartik\socialModule. There is no kartik
folder, but kartik is an alias for $vendorDir . /kartik-v/yii2-social, which when combined with
/social/Module, provides the location of the class.
The actual widget is named FacebookPlugin and that is named spaced at the top of the file:
use kartik\social\FacebookPlugin;
The alias used in the extension works in the namespace as well. So that is how Yii knows where to
find everything. Anyway, the site index page should not be returning an error, once you have all the
above changes in place.
HTML Helper
Ok, moving back to the index.php file for site. Lets make note of the fact that I made the Get Started
Today button conditional on being logged out, no need to display it if logged in, since it links to the
signup form. Also, I used the HTML class that we identified in our Use statement:
use yii\helpers\Html;
Then I used the a method of the Html class to format the link:
<p>
<?php
if (Yii::$app->user->isGuest){
echo Html::a('Get Started Today', ['site/signup'],
['class' => 'btn btn-lg btn-success']);
}
?>
</p>
3 parameters for method a, the first one is the text of the link, Get Started Today. The next one is
the controller/action, site/signup in this case. And then we get the class for the css, which, as we
see in the code, is a button.
A couple of things to note. The a method is pretty smart. It knows that if you are in a profile view.php
file, you can just hand it the action you want, for example, update, and it will know will route you to
the correct update action. Also, note that you can add, separated by a comma, an option parameter
in that array to hand in the get variable, for example:
Ok, moving down the remainder of the page, remove this code:
<div class="body-content">
<div class="row">
<div class="col-lg-4">
<h2>Heading</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed
do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur.</p>
<p><a class="btn btn-default" href="http://www.yiiframework.com/doc/">
Yii Documentation »</a></p>
</div>
<div class="col-lg-4">
<h2>Heading</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed
do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur.</p>
<p><a class="btn btn-default" href="http://www.yiiframework.com/forum/">
Yii Forum »</a></p>
</div>
<div class="col-lg-4">
<h2>Heading</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed
do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat. Duis aute irure dolor in
230
Replace it with:
Gist:
Site Index Remainder
From book:
<?php
echo Collapse::widget([
'items' => [
[
'label' => 'Top Features' ,
'content' => FacebookPlugin::widget([
'type'=>FacebookPlugin::SHARE,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]),
// open its content by default
//'contentOptions' => ['class' => 'in']
],
// another group item
[
231
Modal::begin([
'header' => '<h2>Latest Comments</h2>',
'toggleButton' => ['label' => 'comments'],
]);
echo
FacebookPlugin::widget([
'type'=>FacebookPlugin::COMMENT,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]);
Modal::end();
?>
<br/>
<br/>
<?Php
232
echo Alert::widget([
'options' => [
'class' => 'alert-info',
],
'body' => 'Launch your project like a rocket...',
]);
?>
<div class="body-content">
<div class="row">
<div class="col-lg-4">
<h2>Free</h2>
<p>
<?php
if (!Yii::$app->user->isGuest) {
echo Yii::$app->user->identity->username . ' is doing cool stuff. ';
}
?>
Starting with this free, open source Yii 2 template and it will save you
a lot of time. You can deliver projects to the customer quickly, with
a lot of boilerplate already taken care of for you, so you can concentrate
on the complicated stuff.</p>
<p>
<a class="btn btn-default"
href="http://www.yiiframework.com/doc-2.0/guide-index.html">
Yii Documentation »</a>
</p>
<?php
echo FacebookPlugin::widget([
233
'type'=>FacebookPlugin::LIKE,
'settings' => []
]);
?>
</div>
<div class="col-lg-4">
<h2>Advantages</h2>
<p>
Ease of use is a huge advantage. We've simplifiled RBAC and given you Free/Paid
user type out of the box. The Social plugins are so quick and easy to install,
you will love it!
</p>
<p>
<a class="btn btn-default"
href="http://www.yiiframework.com/forum/">Yii Forum »</a>
</p>
<?php
echo FacebookPlugin::widget([
'type'=>FacebookPlugin::COMMENT,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]);
?>
</div>
<div class="col-lg-4">
<h2>Code Quick, Code Right!</h2>
<p>
234
235
Leverage the power of the awesome Yii 2 framework with this enhanced template.
Based Yii 2's advanced template, you get a full frontend and backend
implementation that features rich UI for backend management.
</p>
<p>
<a class="btn btn-default"
href="http://www.yiiframework.com/extensions/">Yii Extensions »</a>
</p>
</div>
</div>
</div>
</div>
Just a reminder, and this is not part of the code or page, dont forget to save. Also, make sure you
use your domain in the social widgets, not Yii2build.com.
Collapse Widget
You can see in the above code that I referenced a collapse widget, so you need to put the use statement
at the top of the file:
use \yii\bootstrap\Collapse;
Which allows you to reference the widget directly through the static call:
echo Collapse::widget
236
<?php
echo Collapse::widget([
'items' => [
[
'label' => 'Top Features' ,
'content' => FacebookPlugin::widget([
'type'=>FacebookPlugin::SHARE,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]),
// open its content by default
//'contentOptions' => ['class' => 'in']
],
// another group item
[
'label' => 'Top Resources',
'content' => FacebookPlugin::widget([
'type'=>FacebookPlugin::SHARE,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]),
// 'contentOptions' => [],
// 'options' => [],
],
]
]);
We give the items labels, Top Features and Top Resources and then for content we plug in the
237
Modal Widget
After that we use a modal with a button to hold facebook comments, again very easy code to
understand:
Modal::begin([
'header' => '<h2>Latest Comments</h2>',
'toggleButton' => ['label' => 'comments'],
]);
echo
FacebookPlugin::widget([
'type'=>FacebookPlugin::COMMENT,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]);
Modal::end();
Alert Widget
Then I just wanted to play around with an alert widget, so I included:
<?Php
echo Alert::widget([
'options' => [
'class' => 'alert-info',
],
'body' => 'Launch your project like a rocket...',
]);
?>
Probably not how we would actually use an alert, it would be tied to an action, but including it like
this gives us an idea of what it looks like.
Then the last thing we did on index.php was this line in the first <p>
238
<p>
<?php
if (!Yii::$app->user->isGuest) {
echo Yii::$app->user->identity->username . ' is doing cool stuff. ';
}
?>
Just another little difference to test being logged in or out and to make sure its getting the right
username. And thats it for our starting template for the index page.
Font-Awesome
Yii 2 has a somewhat complex set of methods for handling assets like bootstrap, jquery, etc., and
they did this to maximize efficiency by caching resources. Im not going to cover it much in this
book, but we will dabble in it to the extent that we want to have access to font-awesome, a popular
css icon library.
Ok, so heres what we need to make this work.
First, if you have not already done so, we are going to pull in font-awesome via composer, add the
following to your composer.json file:
"minimum-stability": "stable",
"require": {
"php": ">=5.4.0",
"yiisoft/yii2": "*",
"yiisoft/yii2-bootstrap": "*",
"yiisoft/yii2-swiftmailer": "*",
"kartik-v/yii2-social": "dev-master",
"fortawesome/font-awesome": "4.2.0"
},
Now if you look closely, its fortawesome, that is not a typo. Go ahead and run composer update
and this will pull it in for you.
239
Asset Bundle
Then you need to create and add the following file, in two locations, frontend/assets and backend/assets, with corresponding namespaces. The file name is FontAwesomeAsset.php. Here is the entire
file:
Gist:
FontAwesomeAsset.php file
From book:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace frontend\assets;
use yii\web\AssetBundle;
/**
* @author Joao Marques<[email protected]>
*/
/**
240
241
Just copy a second version to backend\assets and set the namespace of that copy to:
namespace backend\assets;
FontAwesomeAsset
So this is an asset bundle. I should note that I got this from the Yii 2 forum, the author of this file is
listed near the top of the file. He did a wonderful job of commenting the code:
http://www.yiiframework.com/forum/index.php/topic/57902-using-fontawesome/
You can also check out more on this subject from the Yii 2 guide:
http://www.yiiframework.com/doc-2.0/guide-structure-assets.html
And then below that, near the other register call at the additional call to register, like so:
242
AppAsset::register($this);
FontAwesomeAsset::register($this);
And that should do it, we should now have access to font-awesome. So lets test this by inserting
the following:
<i class="fa fa-plug"></i>
Then lets go to frontend\views\site\index.php and add it in the first <h1> tag like so:
<h1>Yii 2 Build <i class="fa fa-plug"></i></h1>
That will do it. Now you should have a home page that looks like this:
243
And just for troubleshooting purposes, I will provide the entire frontend/views/site/index.php for
reference. You should not need to do anything at this point, but if you are missing something, you
can reference this file.
Gist:
Frontend Site Index
From book:
<?php
use
use
use
use
use
\yii\bootstrap\Modal;
kartik\social\FacebookPlugin;
\yii\bootstrap\Collapse;
\yii\bootstrap\Alert;
yii\helpers\Html;
<br/>
<?php
echo FacebookPlugin::widget(['type'=>FacebookPlugin::LIKE,
'settings' => []]); ?>
</div>
<?php
echo Collapse::widget([
'items' => [
[
'label' => 'Top Features' ,
'content' => FacebookPlugin::widget([
'type'=>FacebookPlugin::SHARE,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]),
// open its content by default
//'contentOptions' => ['class' => 'in']
],
// another group item
[
'label' => 'Top Resources',
'content' => FacebookPlugin::widget([
'type'=>FacebookPlugin::SHARE,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]),
// 'contentOptions' => [],
// 'options' => [],
],
]
244
]);
Modal::begin([
'header' => '<h2>Latest Comments</h2>',
'toggleButton' => ['label' => 'comments'],
]);
echo
FacebookPlugin::widget([
'type'=>FacebookPlugin::COMMENT,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]);
Modal::end();
?>
<br/>
<br/>
<?Php
echo Alert::widget([
'options' => [
'class' => 'alert-info',
],
'body' => 'Launch your project like a rocket...',
]);
?>
<div class="body-content">
<div class="row">
<div class="col-lg-4">
<h2>Free</h2>
245
246
<p>
<?php
if (!Yii::$app->user->isGuest) {
echo Yii::$app->user->identity->username . ' is doing cool stuff. ';
}
?>
Starting with this free, open source Yii 2 template and it will save you
a lot of time. You can deliver projects to the customer quickly, with
a lot of boilerplate already taken care of for you, so you can concentrate
on the complicated stuff.</p>
<p>
<a class="btn btn-default"
href="http://www.yiiframework.com/doc-2.0/guide-index.html">
Yii Documentation »</a>
</p>
<?php
echo FacebookPlugin::widget([
'type'=>FacebookPlugin::LIKE,
'settings' => []
]);
?>
</div>
<div class="col-lg-4">
<h2>Advantages</h2>
<p>
Ease of use is a huge advantage.
</p>
<p>
<a class="btn btn-default"
href="http://www.yiiframework.com/forum/">
Yii Forum »</a>
</p>
<?php
echo FacebookPlugin::widget([
'type'=>FacebookPlugin::COMMENT,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]);
?>
</div>
<div class="col-lg-4">
<h2>Code Quick, Code Right!</h2>
<p>
Leverage the power of the awesome Yii 2 framework with this enhanced template.
Based Yii 2's advanced template, you get a full frontend and backend
implementation that features rich UI for backend management.
</p>
<p>
<a class="btn btn-default"
href="http://www.yiiframework.com/extensions/">
Yii Extensions »</a>
</p>
247
248
</div>
</div>
</div>
</div>
Summary
Commit!
In this chapter, we configured our Facebook app and installed our Kartik social widget extension so
we could make use of it. With the ease of use of composer, installing the extension was easy. Kartiks
extension is very useful, and as a reminder, you can check many of his other great extensions at:
http://www.krajee.com
We inserted the social widget inside the collapse widget to have a little fun decorating the home
page. You can get more bootstrap widgets to use for your projects at:
http://www.yiiframework.com/doc-2.0/guide-widget-bootstrap.html
Since we already brought in the jui extension for DatePicker, we have access to all the widgets listed
at:
http://www.yiiframework.com/doc-2.0/guide-widget-jui.html
Yii 2 supports a nice number of Jquery and Bootstrap widgets like collapse and modal. This book
isnt really about front end development, so we didnt go too deep, but at least you got a sample to
play with.
Finally, we add an app asset for font-awesome, so you can add some sizzle to presentation, without
too much effort. You can find a lot of wonderful icons to add to your presentation at:
Font-Awesome
Profile
Role
Status
User
UserType
250
User CRUD
If the above image is not clear, the sample looks like this:
Model Class: common\models\User
Search Model Class: backend\models\search\UserSearch
Controller Class: backend\controllers\UserController
We reference common\models\user, since that is where our user model resides, but we are creating
all these other files into the backend. You can see that we also need to provide an entry for a search
model. Make sure you have created the search folder within backend/models before running Gii.
Yii2 provides a separate class for search parameters and Im really glad they did this. It separates out
a lot of code out of the base model, which makes it easier to follow. You will see how it works later.
Note that we can leave the view path blank because we are conforming to the default.
To make sure you have the right namespaces and file locations generated, I will list out all the
remaining models that you need to generate CRUD from, with namespaces specified.
Profile:
Model Class: frontend\models\Profile
251
User:
Model Class: common\models\User
Search Model Class: backend\models\search\UserSearch
Controller Class: backend\controllers\UserController
Role:
Model Class: backend\models\Role
Search Model Class: backend\models\search\RoleSearch
Controller Class: backend\controllers\RoleController
Status:
Model Class: backend\models\Status
Search Model Class: backend\models\search\StatusSearch
Controller Class: backend\controllers\StatusController
User Type:
Model Class: backend\models\UserType
Search Model Class: backend\models\search\UserTypeSearch
Controller Class: backend\controllers\UserTypeController
Since the process of creating CRUD is exactly the same for each model listed above, we wont go
through each one here. At this point, we will just assume you have created the files as we move on
from here.
One thing to note. The naming convention for views with multiple words in the name is to put a - in
between the two words, so the view folder for UserType model is user-type. The urls for controllers
are this way too. Even though the controller file is named UserTypeController, to reach it by url,
you would call, for example
backend.yii2build.com/index.php?r=user-type/index
252
Ok, you can check the results individually by typing in a url, for example:
backend.yii2build.com/index.php?r=user
This will call the index action and assuming you have at least one record in there, it will display the
record. Obviously, you would have to login to backend.yii2build.com and the user logging in would
have to have a role_id that matches a role named Admin, since our PermissionHelper enforces that
rule.
The user index page also has links to view and update and delete, those are the icons you see on the
right of the grid, so you can check to see if these are working as well. Then try the same url above
with the different models you created to make sure its all working.
If you get errors, check the locations of your files and check each file for namespaces. For example,
in ProfileController.php:
namespace backend\controllers;
use
use
use
use
use
use
Yii;
frontend\models\Profile;
backend\models\search\ProfileSearch;
yii\web\Controller;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
You can see its using frontend\models for the model, but backend\models\search for the search
model.
While its possible to duplicate a model into more than one location, its definitely not recommended,
it defeats the purpose of having a common folder and violates the principle of DRY.
I used the frontend\models for Profile, but could have used common\models. I just chose the former
because of workflow. But you can see how easy it is to reference the correct folder via namespace
when you are creating the controller and views.
We are going to make some changes to the view files and to the search model, but the controllers
will mostly be left as is, except for behaviors. That is because the controllers and views that Gii
creates lend themselves to a backend approach, that is one logged-in user who can search a list of
users, update all records, etc. The out of the box controllers allow for this, so the good news that the
auto-code generation is really helpful and a great time-saver.
So by simply controlling access to the admin area by enforcing a minimum value to role_id on the
user record, only admin-level users can access the backend controllers. We will also demonstrate
how easy it is to add a role above admin that can change records that admin cant. In our template,
users with Admin-level access can use the backend UI to change user records.
We will of course be restricting that somewhat, for example, admin will not be able to change a users
password, they wont even see it. We will make several changes in our backend UI to make moving
253
around the related records easier, for example, we will want to have access to a users role_id and
the ability to change it, so we can grant additional privileges to users.
You can see just how powerful Yii 2 really is by allowing us to set all this up quickly. And because
we took the time earlier to make models for things like role and status, we are now going to have a
full backend UI to control them.
Before we change the individual view files, lets make some changes to backend/views/layout/main.php. We will add numerous links to make it easy for us to navigate through the different
views.
Main.php
Go to backend/views/layout/main.php.
Change main.php to:
Gist:
backend main view change 1
<?php
use
use
use
use
use
use
use
backend\assets\AppAsset;
yii\helpers\Html;
yii\bootstrap\Nav;
yii\bootstrap\NavBar;
yii\widgets\Breadcrumbs;
common\models\PermissionHelpers;
backend\assets\FontAwesomeAsset;
/**
* @var \yii\web\View $this
* @var string $content
*/
AppAsset::register($this);
FontAwesomeAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<?php
if (!Yii::$app->user->isGuest){
$is_admin = PermissionHelpers::requireMinimumRole('Admin');
NavBar::begin([
'brandLabel' => 'Yii 2 Build <i class="fa fa-plug"></i> Admin',
'brandUrl' => Yii::$app->homeUrl,
'options' => [
'class' => 'navbar-inverse navbar-fixed-top',
],
]);
} else {
254
255
NavBar::begin([
'brandLabel' => 'Yii 2 Build <i class="fa fa-plug"></i>',
'brandUrl' => Yii::$app->homeUrl,
'options' => [
'class' => 'navbar-inverse navbar-fixed-top',
],
]);
$menuItems = [
['label' => 'Home', 'url' => ['site/index']],
];
}
if (!Yii::$app->user->isGuest &&
$is_admin) {
}
echo Nav::widget([
'options' => ['class' => 'navbar-nav navbar-right'],
'items' => $menuItems,
]);
NavBar::end();
?>
<div class="container">
<?= Breadcrumbs::widget([
'links' => isset($this->params['breadcrumbs']) ?
$this->params['breadcrumbs'] : [],
])?>
<?= $content ?>
</div>
</div>
<footer class="footer">
<div class="container">
<p class="pull-left">© Yii 2 Build <?= date('Y') ?></p>
<p class="pull-right"><?= Yii::powered() ?></p>
</div>
</footer>
<?php $this->endBody() ?>
256
257
</body>
</html>
<?php $this->endPage() ?>
Youll note Ive used a lot of whitespace to make the code easier to read. Keep in mind, readability
is impacted by being in book format.
We also added a use statement:
use common\models\PermissionHelpers;
The logic behind this is if not a guest and the current user has role_id greater than or equal to the
one need for admin, display the link. Its a very simple way to control access to the links.
if (!Yii::$app->user->isGuest && $is_admin) {
$menuItems[] = ['label' => 'Users', 'url' => ['user/index']];
$menuItems[] = ['label' => 'Profiles', 'url' => ['profile/index']];
$menuItems[] = ['label' => 'Roles', 'url' => ['/role/index']];
$menuItems[] = ['label' => 'User Types', 'url' => ['/user-type/index']];
$menuItems[] = ['label' => 'Statuses', 'url' => ['/status/index']];
}
258
We use the $menuItems array to hold the url because we are working in the NavBar widget.
Note, we skipped over the block where we added an if statement to see if the user was logged in or
not, then show them either:
'brandLabel' => 'Yii 2 Build Admin',
I just did that because even though its only cosmetic, I dont even like acknowledging the word
admin to users who are not logged in.
backend/views/profile/_form.php
Lets start with the easiest first. We will simply copy frontend/views/profile/_form.php into
backend/views/profile/_form.php.
So now we have the jui datepicker and the drop down list for gender in the form, which is exactly
what we wanted.
backend/views/profile/view.php
Next, lets work on view.php for backend/views/profile/view.php. This is what Gii gave us out of
the box:
<?php
use yii\helpers\Html;
use yii\widgets\DetailView;
/* @var $this yii\web\View */
/* @var $model frontend\models\Profile */
$this->title = $model->id;
$this->params['breadcrumbs'][] = ['label' => 'Profiles', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="profile-view">
<h1><?= Html::encode($this->title) ?></h1>
<p>
<?= Html::a('Update', ['update', 'id' => $model->id],
['class' => 'btn btn-primary']) ?>
<?= Html::a('Delete', ['delete', 'id' => $model->id],
['class' => 'btn btn-danger',
'data' => [
'confirm' => 'Are you sure you want to delete this item?',
'method' => 'post',
],
]) ?>
</p>
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'user_id',
'first_name:ntext',
'last_name:ntext',
'birthdate',
'gender_id',
'created_at',
'updated_at',
],
]) ?>
259
</div>
<p>
<?php if (!Yii::$app->user->isGuest && $show_this_nav) {
echo Html::a('Update', ['update', 'id' => $model->id],
['class' => 'btn btn-primary']);}?>
260
261
'confirm' => Yii::t('app', 'Are you sure you want to delete this item?'),
'method' => 'post',
],
]);}?>
</p>
<?= DetailView::widget([
'model' => $model,
'attributes' => [
['attribute'=>'userLink', 'format'=>'raw'],
'first_name',
'last_name',
'birthdate',
'gender.gender_name',
'created_at',
'updated_at',
'id',
],
])?>
</div>
It doesnt hurt to maintain consistency in wrapping the nav in if statements that check to see if the
user has permission to do the action, before showing the links.
So to demonstrate this fully, we are going to require a higher role than admin to update or delete
profiles. Lets call it SuperUser.
Go ahead and create a user in the application and through PhpMyAdmin, enter a role record for
SuperUser and assign the new user that value. Dont forget it has to be a higher value than 20,
which is what we gave Admin. Lets use 30 in the role table for the role_value for example.
role table
262
Later, we will also modify our controller later to not allow anyone without admin access to view this
page and I like not showing the link if it is not available to the user, so we have consistent behavior
between the nav and access rules on the controller.
Ok, moving along through the changes, we changed the title to:
$this->title = $model->user->username;
Under that, we added the call for the user to be at least SuperUser role in order to see the update
link:
$show_this_nav = PermissionHelpers::requireMinimumRole('SuperUser');
So now it displays user name instead of profile id number, much more user friendly.
Then we added our links to update and delete:
<p>
<?php if (!Yii::$app->user->isGuest && $show_this_nav) {
echo Html::a('Update', ['update', 'id' => $model->id],
['class' => 'btn btn-primary']);}?>
Moving on, we changed the DetailView::widget. We changed the format of the first attribute to:
['attribute'=>'userLink', 'format'=>'raw'],
Our app knows what we are referring to because we added the getUserLink method to the Profile
model and created the label:
'userLink' => Yii::t('app', 'User'),
263
This method returns the link to the user view page that we want. Im showing the label and method
here because this is probably where in your workflow you would have created them, since this is
where you would see that you need them. We obviously built these in advance, so they are already
in place. In the future, if you can anticipate the need for these kinds of methods, you can create them
in advance as we did, as part of a bunch of boilerplate methods that you always add to model, when
you create a model. Your workflow decisions, however, are best left up to you.
The other big change is a lazy loading relationship:
'gender.gender_name',
That simply tells it return the gender_name attribute from the Gender model, so we get male
instead of 1, which again, is much more friendly to the user. We have access to this because on the
Profile model, we have the following method:
public function getGender()
{
return $this->hasOne(Gender::className(), ['id' => 'gender_id']);
}
You might be wondering at this point what we meant by lazy loading relationship. A lazy loading
query will do a DB query for each row of results, which is highly inefficient. A 1000 results would
require 1001 queries (also known as the n+1 problem).
Its ok to do lazy loading when there is only one result, but you have to be careful about it. We will
demonstrate the eager loading version of a query when we do the Index page.
backend/views/user/view.php
Moving on to the backend/views/user/view.php page, lets change it to the following:
Gist:
Backend User View.php
From book:
<?php
use yii\helpers\Html;
use yii\widgets\DetailView;
use common\models\PermissionHelpers;
/* @var $this yii\web\View */
/* @var $model common\models\user */
$this->title = $model->username;
$show_this_nav = PermissionHelpers::requireMinimumRole('SuperUser');
$this->params['breadcrumbs'][] = ['label' => 'Users', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="user-view">
<h1><?= Html::encode($this->title) ?></h1>
<p>
<?php if (!Yii::$app->user->isGuest && $show_this_nav) {
echo Html::a('Update', ['update', 'id' => $model->id],
['class' => 'btn btn-primary']);}?>
<?php if (!Yii::$app->user->isGuest && $show_this_nav) {
echo Html::a('Delete', ['delete', 'id' => $model->id], [
'class' => 'btn btn-danger',
'data' => [
'confirm' => Yii::t('app', 'Are you sure you want to delete this item?'),
'method' => 'post',
],
]);}?>
</p>
<?= DetailView::widget([
'model' => $model,
'attributes' => [
['attribute'=>'profileLink', 'format'=>'raw'],
//'username',
264
265
//'auth_key',
//'password_hash',
//'password_reset_token',
'email:email',
'roleName',
'statusName',
'userTypeName',
'created_at',
'updated_at',
'id',
],
]) ?>
</div>
The use statements are exactly the same as the profile view page. Slight change to $title, since we
want to display the user name, its an attribute of the current model:
$this->title = $model->username;
We got rid of unwanted fields displaying by commenting them out. We could cut them out entirely,
but I like to leave these in for debug purposes, if I ever need to recall them.
266
So obviously our first attribute is a link to profile, which because of the methods we placed on the
user model in the beginning, allow us to reference them as profileLink. Since this is identical to what
we did previously in the Profile view, I will not explain that further, but you can look at the methods
on the user model to refresh your knowledge.
Note on the email attribute, we use email:email and this formats a mailto link on the address as it
displays in the view, a nice handy feature.
We see roleName and statusName are brought in through relationships again defined on the user
model.
When you check this all out in your browser on your project, notice how easy it is to move from
the user to profile views by having those items linked. This is good UI practice and end users will
appreciate it.
backend/views/user/_form
Lets get the _form for user view updated to have dropdowns. Replace the existing file with the
following:
Gist:
Backend User _form View
From book:
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/**
* @var yii\web\View $this
* @var common\models\User $model
* @var yii\widgets\ActiveForm $form
*/
?>
<div class="user-form">
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'status_id')->dropDownList($model->statusList,
[ 'prompt' => 'Please Choose One' ]);?>
267
This is a simple use of the ActiveForm widget, which weve seen before when we were looking at
our first view file. You should be used to this format by now. Note that I used 2 lines instead of one
to avoid wordwrapping errors that happen when this book tries to adjust wordwrap in code.
Note the use of the Html helper class on the submit button. A nice ternary statement determines if
the record is new or needs to be updated.
One other thing to point out on this. We do not need to id the action on the form. Because of Yii 2s
framework logic, it knows what model and action to associate this form with, based on the location
of the file and the model passed to the view.
Its only when your forms are more complicated that you need to create a separate form model thats
when it needs an id on the form, so the controller knows which model to use. We saw examples of
this from site controller, where there were numerous form models being used for things like contact,
requestPasswordReset, etc.
backend/views/user/index.php
So lets replace the file in backend/views/user/index.php with:
Gist:
Backend User Index View
From book:
<?php
use yii\helpers\Html;
use yii\grid\GridView;
use \yii\bootstrap\Collapse;
/* @var $this yii\web\View */
/* @var $searchModel backend\models\search\UserSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */
$this->title = 'Users';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="user-index">
<h1><?= Html::encode($this->title) ?></h1>
<?php
echo Collapse::widget([
'items' => [
// equivalent to the above
[
'label' => 'Search',
'content' => $this->render('_search', ['model' => $searchModel]) ,
// open its content by default
//'contentOptions' => ['class' => 'in']
],
]
]);
?>
268
269
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
//'id',
['attribute'=>'userIdLink', 'format'=>'raw'],
['attribute'=>'userLink', 'format'=>'raw'],
['attribute'=>'profileLink', 'format'=>'raw'],
'email:email',
'roleName',
'userTypeName',
'statusName',
'created_at',
['class' => 'yii\grid\ActionColumn'],
// 'updated_at',
],
]); ?>
</div>
This lets us use the collapse widget, which we use to hold render statement, which brings in the
form partial for search. The net effect is that it cleans up the page. In the view, when you mouseover
the word search, it turns into a link. Click it, and the search form drops down. Since we already
covered the collapse widget in a previous chapter, we wont go over it again.
We will be making changes to our search form, so at this point no need to bother testing it.
In the GridView widget, we left some attributes commented out for reference. You can see we added
userIdLink, userLink, ProflieLink, email:email, roleName, userTypeName, and statusName. These
are the labels we gave the methods on the User model attributes method. In the case of userIdLink,
userLink, and profileLink, we have a specific format that we have to use to return the link. The
270
email:email format creates a mailto link, handy if you want to email the user. The method for
userLink displays the username, in case you are wondering about that.
backend/views/profile/index.php
Lets do something similar for Profile, while were at it. Paste in the following to backend/views/profile/index.php:
Gist:
Backend Profile Index View
From book:
<?php
use yii\helpers\Html;
use yii\grid\GridView;
use \yii\bootstrap\Collapse;
/* @var $this yii\web\View */
/* @var $searchModel backend\models\search\ProfileSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */
$this->title = 'Profiles';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="profile-index">
<h1><?= Html::encode($this->title) ?></h1>
<?php
echo Collapse::widget([
'items' => [
// equivalent to the above
[
'label' => 'Search',
'content' => $this->render('_search', ['model' => $searchModel]) ,
// open its content by default
//'contentOptions' => ['class' => 'in']
],
271
]
]); ?>
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
//'id',
['attribute'=>'profileIdLink', 'format'=>'raw'],
['attribute'=>'userLink', 'format'=>'raw'],
'first_name',
'last_name',
'birthdate',
'genderName',
['class' => 'yii\grid\ActionColumn'],
// 'created_at',
// 'updated_at',
// 'user_id',
],
]); ?>
</div>
The GridView widget is similar to the one in the user index view, but with less columns.
When I was doing this in workflow, I noticed we could link the id attribute to the update profile
view, which would give us a fast way in to update profile. So I added the following method to the
profile model:
272
Then finally, I commented out id and added profileIdLink to the widget in backend/views/profile/index:
['attribute'=>'profileIdLink', 'format'=>'raw'],
But most of this was obviously done previously, when we created the models. At least now you
know why we did it.
While this gets us our link, we have no sort capabilities. Since sorting is something we want, we
will make those changes, but we will wait to add that because we will have to make other changes
to the search model at the same time.
backend/views/profile/_search.php
Now lets update backend/views/profile/_search.php. Replace the entire contents of the file with the
following:
Gist:
Backend Profile _search View
From book:
<?php
use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
use frontend\models\Profile;
/**
* @var yii\web\View $this
* @var backend\models\search\ProfileSearch $model
* @var yii\widgets\ActiveForm $form
*/
?>
273
<div class="profile-search">
<?php $form = ActiveForm::begin([
'action' => ['index'],
'method' => 'get',
]); ?>
Dont bother testing search accuaracy yet, we have to work on the search model, we will get to that
shortly.
backend/views/user/_search.php
Ok, so now we move on to working on the _search view file for user and backend/models/search/UserSearch.php file. The UserSearch.php file provides the model for _search.php in the
backend/views/user/_search.php, which itself is rendered inside of backend/views/user/index.php.
Basically, its the search form at the top of the index file.
Lets start by replacing the contents of _search.php with the following:
Gist:
Backend User _search View
From book:
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
use common\models\User;
/* @var $this yii\web\View */
/* @var $model backend\models\search\UserSearch */
/* @var $form yii\widgets\ActiveForm */
?>
<div class="user-search">
<?php $form = ActiveForm::begin([
'action' => ['index'],
'method' => 'get',
]); ?>
<?= $form->field($model, 'id') ?>
<?= $form->field($model, 'username') ?>
<?php echo $form->field($model, 'email') ?>
<?= $form->field($model, 'role_id')->dropDownList(User::getroleList(),
[ 'prompt' => 'Please Choose One' ]);?>
<?= $form->field($model, 'user_type_id')->dropDownList(User::getuserTypeList(),
[ 'prompt' => 'Please Choose One' ]);?>
274
275
<div class="form-group">
<?= Html::submitButton('Search', ['class' => 'btn btn-primary']) ?>
<?= Html::resetButton('Reset', ['class' => 'btn btn-default']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
One thing you might have noticed is that on our ActiveForm::begin method, we are listing action
and method. The reason we are doing this is that we expect dynamic data from the user. They are
going to send get variables along to the controller, so we need to specify get as the method. This is
how Gii hands it to us.
And you can see we have barely changed the file otherwise, except to make dropdown lists from
methods on the User model.
Now if you try this in the browser, it works great, but you will notice that the dropdown for userType
shows the options, but does not filter the results. Also, we need to make sure that we eager load our
results.
Eager loading, if you recall, is how we avoid the n+1 problem, where a query is made for each row
of results. In a database where there are large numbers of results, an n+1 problem can render the
page useless because it will take forever, if ever, to return results.
We get around that by eager loading. We will do this when we modify the UserSearch model.
User Search
The UserSearch model is an extension of the User model, in this case User, that the controller uses
to instruct it on how to query the model.
The file is located at backend/models/search/UserSearch.php. The main method is search($params),
so lets look at that:
276
'auth_key', $this->auth_key])
'password_hash', $this->password_hash])
'password_reset_token', $this->password_reset_token])
'email', $this->email]);
return $dataProvider;
}
}
$params is being handed in by the form. This happens via the UserController, which starts the index
action as follows:
public function actionIndex()
{
$searchModel = new UserSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
To display results, we call an instance of the model, in this case new UserSearch(), then set
$dataProvider variable as an instance of the model, with search method handing in the query
277
ActiveDataProvider creates a powerful iterator out of the object results, in this case user, where we
find all results, since $query was originally set to User::find();.
Once we instantiate the instance of ActiveDataProvider, we check to see if we added any search
parameters through the form, and if not, return the unfiltered result of $query, which as we have
already stated, will return all the users.
if (!($this->load($params) && $this->validate())) {
return $dataProvider;
}
If there are $params handed in from the form, then we evaluate false, which means we dont yet
return $dataProvider, and we move to the next block and then call the andFilterWhere() method on
the user Model, to filter the parameters.
$query->andFilterWhere([
'id' => $this->id,
'role_id' => $this->role_id,
'status_id' => $this->status_id,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
]);
Next we see another call to the same method to cover like as a parameter:
278
You can see how the method calls are chained together successively, with the semicolon on the last
line. Then finally, we return $dataprovider:
return $dataProvider;
So thats the out of the box version. But we need a more robust version of this. We have to get related
data from Roles, UserType, etc. and we need eager loading, so what we need is a little more complex.
Replace the old UserSearch model with the following:
Gist:
Backend User Search Model
From book:
<?php
namespace backend\models\search;
use
use
use
use
Yii;
yii\base\Model;
yii\data\ActiveDataProvider;
common\models\User;
/**
* UserSearch represents the model behind the
*search form about `common\models\user`.
*/
*/
public
public
public
public
public
public
$roleName;
$userTypeName;
$user_type_name;
$user_type_id;
$statusName;
$profileId;
/**
* @inheritdoc
*/
/**
* @inheritdoc
*/
279
$dataProvider->setSort([
'attributes' => [
'id',
'userIdLink' => [
'asc' => ['user.id' => SORT_ASC],
'desc' => ['user.id' => SORT_DESC],
'label' => 'User'
],
'userLink' => [
'asc' => ['user.username' => SORT_ASC],
'desc' => ['user.username' => SORT_DESC],
'label' => 'User'
],
'profileLink' => [
'asc' => ['profile.id' => SORT_ASC],
'desc' => ['profile.id' => SORT_DESC],
'label' => 'Profile'
],
280
'roleName' => [
'asc' => ['role.role_name' => SORT_ASC],
'desc' => ['role.role_name' => SORT_DESC],
'label' => 'Role'
],
'statusName' => [
'asc' => ['status.status_name' => SORT_ASC],
'desc' => ['status.status_name' => SORT_DESC],
'label' => 'Status'
],
'userTypeName' => [
'asc' => ['user_type.user_type_name' => SORT_ASC],
'desc' => ['user_type.user_type_name' => SORT_DESC],
'label' => 'User Type'
],
'created_at' => [
'asc' => ['created_at' => SORT_ASC],
'desc' => ['created_at' => SORT_DESC],
'label' => 'Created At'
],
'email' => [
'asc' => ['email' => SORT_ASC],
'desc' => ['email' => SORT_DESC],
'label' => 'Email'
],
]
]);
if (!($this->load($params) && $this->validate())) {
$query->joinWith(['role'])
->joinWith(['status'])
->joinWith(['profile'])
->joinWith(['userType']);
return $dataProvider;
}
$this->addSearchParameter($query, 'id');
281
282
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
'username', true);
'email', true);
'role_id');
'status_id');
'user_type_id');
'created_at');
'updated_at');
// filter by role
$query->joinWith(['role' => function ($q) {
$q->andFilterWhere(['=', 'role.role_name', $this->roleName]);
}])
// filter by status
->joinWith(['status' => function ($q) {
$q->andFilterWhere(['=', 'status.status_name', $this->statusName]);
}])
// filter by user type
->joinWith(['userType' => function ($q) {
$q->andFilterWhere(['=', 'user_type.user_type_name', $this->userTypeName]);
}])
// filter by profile
->joinWith(['profile' => function ($q) {
$q->andFilterWhere(['=', 'profile.id', $this->profileId]);
}]);
return $dataProvider;
}
283
$attribute = "user.$attribute";
if ($partialMatch) {
$query->andWhere(['like', $attribute, $value]);
} else {
$query->andWhere([$attribute => $value]);
}
}
}
Ok, lets tackle this beast. It seems like a lot, but its not bad once you break it down. Also, I should
note that I learned this by following a tutorial in the wiki by Kartik, the same author who wrote
the social widget that we are using on our applications home page. I refactored only slightly for
cosmetic appeal, and we probably didnt gain much clarity from that, but at least I tried.
The first thing to note is that class UserSearch extends User, and we made sure to include in our list
of attributes those attributes which are referenced by a method on the model, roleName for example,
because we know we are going to use the dropdown list to return role names. If this is not explicitly
listed as an attribute, the form breaks and the page does not render. So if you are running into that
type of problem, make sure you have a complete list of attributes that it needs. This is not always
obvious because the parent model of User is supposed to know all of its attributes from reflection
somewhere in the base classes.
What I found, putting this together, is that I need to declare the attributes $user_type_id and $user_-
284
type_name. Im just not sure why. I use these attributes in the where clauses, so maybe that is the
source of the problem, perhaps it cant use the parent model at that location to identify the attribute.
This of course, is just a guess.
When youre working with a large framework like Yii 2, you are occasionally going to run up against
things that you dont completely understand. Its ok, it happens to everyone, I can certainly attest
to that personally. The important thing is that we try to learn as much as we can as we go along
because when it comes to using a framework, knowledge is power.
Ok, next up are the rules used by validation. The first array tells us which attributes are integer
only. The second array tells which attributes are safe. We need this rule because of the setAttributes
method in the /yii/base/Model class, which ignores attributes if an attribute does not have at least
one validation rule and is not marked safe by the safe rule. So when the contents of $_Post are sent
to the method, only the accepted values will be allowed in.
Anyway, Gii includes a safe array in the rules that it auto-generates, so I have kept this array, and
made sure that it contains the current attributes, which are in addition to those on the parent model.
Again I had to use trial and error to make sure I had what I needed.
Next we have the scenarios method:
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
This method allows you to bypass the parent scenarios, which would allow you create your own
scenarios. We wont be using this, so well just leave it in place, since this is how Gii gave it to us.
Next we would have the attributeLabels method, but we dont need one because we are inheriting
all the attribute labels we need from the parent model and we havent added anything new that
would require one.
Ok, lets move on the search method:
public function search($params)
{
$query = User::find();
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
This is exactly like the code Gii generates for us, so no changes there. We create an instance of User
with the find method, which will return all results. Then we create an instance of ActiveDataProvider
285
and inject the user model via $query. So now $dataProvider is loaded up with the User model and
all its records. Later, we will use the controller to pass this $dataProvider to the view, where the
GridView widget can use it.
Ok, back to the UserSearch model and its search method.
Next we take the setSort method of $dataProvider and configure it so that the columns we want to
be sortable on the Grid will have the behavior that we want:
$dataProvider->setSort([
'attributes' => [
'id',
'userIdLink' => [
'asc' => ['user.id' => SORT_ASC],
'desc' => ['user.id' => SORT_DESC],
'label' => 'ID'
],
'userLink' => [
'asc' => ['user.username' => SORT_ASC],
'desc' => ['user.username' => SORT_DESC],
'label' => 'User'
],
'profileLink' => [
'asc' => ['profile.id' => SORT_ASC],
'desc' => ['profile.id' => SORT_DESC],
'label' => 'Profile'
],
'roleName' => [
'asc' => ['role.role_name' => SORT_ASC],
'desc' => ['role.role_name' => SORT_DESC],
'label' => 'Role'
],
'statusName' => [
'asc' => ['status.status_name' => SORT_ASC],
'desc' => ['status.status_name' => SORT_DESC],
'label' => 'Status'
],
'userTypeName' => [
'asc' => ['user_type.user_type_name' => SORT_ASC],
'desc' => ['user_type.user_type_name' => SORT_DESC],
286
We saw this before when we looked at the version Gii gave us. This one operates the same way.
What it says is load the parameters and run validation method, then evaluate true or false. The ! can
be confusing, so I will explain it fully.
If the statement evaluates to true, there is only a small amount of code that follows to a return
statement. This is very easy to read. Just to be clear, no parameters would evaluate true, then you
would execute the small block of code with the return statement.
287
If the if statement evaluates false, and there are parameters being handed in from the search form,
and we move onto the next block of code to follow.
Ok, so lets deal with the true condition first:
If no parameters evaluates true from the if statement, add the relationships via joinWith method
(for eager loading) and return the model with its relationships, stored in the $dataProvider, since
$query is already injected into $dataProvider. The controller will pass $dataProvider to the view.
{
$query->joinWith(['role'])
->joinWith(['status'])
->joinWith(['profile'])
->joinWith(['userType']);
return $dataProvider;
}
If you look carefully, you see that userType has the capital T, which has to do with how the model
relation is named because there are two words involved in its composition. If you name that wrong
or if there is no get method for the relation on the user model, you will get an error. The naming
convention gives a capital letter to the second word in a model name when there is more than one
word in the model.
$query is an instance of the User model and is configured into $dataProvider, so even if we hand in
no parameters, we can still return unfiltered results. So again, to be perfectly clear, if there are no
parameters for search, we return all user records.
Note we have chained together the user model with 4 other models, role, status, profile, and userType,
so we can eager load our results, which means we wont have to do a separate query for each row
of results. These joins allow us sync the User with the appropriate profile, role, etc.
Eager loading is the opposite of lazy load, and for large record sets, like the records in the User Model
for example, this is preferable because it puts less strain on the DB.
Now lets look at the longer, more complex possibility of the if statement. If the if (!($this>load($params) && $this->validate())) statement evaluates false, this means we have parameters
for the search, and we move on to the next block of code, where we use a method named
addSearchParameter to add conditions to the query:
288
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
'id');
'username', true);
'email', true);
'role_id');
'status_id');
'user_type_id');
'created_at');
'updated_at');
You can see we run one instance of the method for each attribute. So lets look at the addSearchParameter method to get a better idea of whats going on:
protected function addSearchParameter($query, $attribute, $partialMatch = false)
{
if (($pos = strrpos($attribute, '.')) !== false) {
$modelAttribute = substr($attribute, $pos + 1);
} else {
$modelAttribute = $attribute;
}
$value = $this->$modelAttribute;
if (trim($value) === ' ') {
return;
}
/*
* The following line is additionally added for right aliasing
* of columns so filtering happen correctly in the self join
*/
$attribute = "user.$attribute";
if ($partialMatch) {
$query->andWhere(['like', $attribute, $value]);
} else {
$query->andWhere([$attribute => $value]);
}
}
The first part determines if there is a . in the attribute. I added whitespace to make it easier to read:
289
If the parameter has a . , then the method positions the attribute to the search parameter correctly,
so it knows what is the model and what is the attribute.
The method is eliminating what goes before the period, which sets it up to eliminate an ambiguation
problem, since the role table for example, also has an id column. This is tricky stuff and it wont
work right if we dont do it exactly this way.
Whether it has a . or not, it sets $attribute to $modelAttribute.
The next line:
$value = $this->$modelAttribute;
Sets the value of the attribute. Just a quick reminder on how this works.
The attribute is handed in to this method as string to the variable $attribute, where it is formatted
to account for whether or not there was a period.
In either case, whether or not it had a period or not, the variable is renamed $modelAttribute.
But this still represents the string that was handed in through $attribute. So when we call $this>$modelAttribute, we are inserting the variable where a string would normally go. The variable
$value picks up the result of this expression, whatever type it may be, string, integer, bool.
For example, if we read $value = $this->username, it would be more intuitive for us to expect the
$value has the actual username, which is a string. Instead we got $value = $this->$modelAttribute,
which is great because we can use it for all the attributes and it will pass the correct format to the
$value variable.
$this is referring to an instance of UserSearch, which as we know, extends User, so $this can have
an attribute named username or any of the other ones we provided when we called the method.
Now you may be asking yourself, if you are using a string, for example, how does it know which
specific value to return? This is a real brain teaser for me, not so obvious from staring at the code.
The answer is that it has already acquired the value from the form:
if (!($this->load($params) && $this->validate()))
Remember that the not ! statement only evaluates true, forcing the return statement, if there are no
parameters. If there are parameters, it successfully runs the load and validate methods, so by the
time we are using these field names to set up our query in the addSearchParameter method, the
model, our friendly $this, already has the values we need.
290
Ah, so simple once we see how it works. I dont know if you struggled over it as I did, but for both our
sakes, Im glad I finally got it. Also note, if the parameters cant load because of validation failure,
it will show the form with errors. However this action takes place in the controller, not the model.
Anyway, back to the addSearchParameter method. We havent finished it yet:
Return if empty:
if (trim($value) === ' ') {
return;
}
In this case, thats another way of saying do nothing, so you dont end up with a bunch of blank
where statements on the query. Again to be clear, if the field is empty, it does not get added as a
search parameter.
Otherwise:
/*
* The following line is additionally added for right aliasing
* of columns so filtering happen correctly in the self join
*/
$attribute = "user.$attribute";
The comment above the line explains part of it. We set the table name in front of the attribute to
avoid ambiguation problems. Since this method runs one attribute at a time, we can safely assume
that $attribute holds the string we intended. Since we stripped out anything in front of the period
earlier in the method, there can be no confusion about which table we are referring to, since we are
explicitly telling it to use user.
Ok, on to $partialMatch. The default value is set to false. So the statement if ($partialMatch) will
check to see if it is true. The only way it can be true is if it is handed in that way. If you check the
list of calls to the addSearchParmeter method, you can see that username and email are set to true.
Partial matches are handy, especially on strings, where the user doesnt want or sometimes even
know how to type in the full match.
Anyway, if $partialMatch is true, then use the like operator in the andWhere method (which has
been inherited from somewhere else in the framework) to add a partial match to search on:
291
if ($partialMatch) {
$query->andWhere(['like', $attribute, $value]);
}
Else, use the andWhere method to hand in the attribute and its value to the query:
$query->andWhere([$attribute => $value]);
Ok, so the attributes are added. Now we are back to where we left off in the search method and
we come to the joins that will allow us to filter. You can see that for each one, we add a closure, an
anonymous function, that binds the andFilterWhere method to the model being joined:
// filter by role
$query->joinWith(['role' => function ($q) {
$q->andFilterWhere(['=', 'role.role_name', $this->roleName]);
}])
// filter by status
->joinWith(['status' => function ($q) {
$q->andFilterWhere(['=', 'status.status_name', $this->statusName]);
}])
// filter by user type
->joinWith(['userType' => function ($q) {
$q->andFilterWhere(['=', 'user_type.user_type_name', $this->userTypeName]);
}])
// filter by profile
->joinWith(['profile' => function ($q) {
$q->andFilterWhere(['=', 'profile.id', $this->profileId]);
292
}]);
Note that we stacked the ->joinWtih methods, but we have comments in between, be careful, the
closing semicolon only comes at the very end. This is very intuitive syntax in the andFilterWhere
method. The first parameter gives us our operator, in this case = means equal because we are building
a sql query. Second parameter gives us tablename and field. Third parameter is value we want bound
to the query. Again, we know the model already has the input values from the form, so when you
see. $this->statusName, for example, it is using the value coming in from the form.
And thank God thats over with. Im exhausted. Learning programming is fun, but its also hard
work.
We need to make similar changes to ProfileSearch.php and we need to make sure we add our sort
for the profileIdLink:
Lets take the entire ProfileSearch.php file and replace it with:
Gist:
Backend Profile Search Model
From book:
<?php
namespace backend\models\search;
use
use
use
use
Yii;
yii\base\Model;
yii\data\ActiveDataProvider;
frontend\models\Profile;
/**
* @inheritdoc
*/
$dataProvider->setSort([
'attributes' => [
'id',
'first_name',
'last_name',
'birthdate',
'genderName' => [
'asc' => ['gender.gender_name' => SORT_ASC],
'desc' => ['gender.gender_name' => SORT_DESC],
'label' => 'Gender'
],
'profileIdLink' => [
'asc' => ['profile.id' => SORT_ASC],
'desc' => ['Profile.id' => SORT_DESC],
'label' => 'ID'
293
294
],
'userLink' => [
'asc' => ['user.username' => SORT_ASC],
'desc' => ['user.username' => SORT_DESC],
'label' => 'User'
],
]
]);
'id');
'first_name', true);
'last_name', true);
'birthdate');
'gender_id');
'created_at');
'updated_at');
'user_id');
295
return $dataProvider;
}
/*
* The following line is additionally added for right aliasing
* of columns so filtering happen correctly in the self join
*/
$attribute = "profile.$attribute";
if ($partialMatch) {
$query->andWhere(['like', $attribute, $value]);
} else {
$query->andWhere([$attribute => $value]);
}
}
}
296
Admin UI
Lets make a change to backend/views/site/index. We want navigation to make our admin tasks
easier, so lets replace the old file with:
Gist:
Backend Site Index View
From book:
<?php
use yii\helpers\Html;
use common\models\PermissionHelpers;
/**
* @var yii\web\View $this
*/
<div class="site-index">
<div class="jumbotron">
<h1>Welcome to Admin!</h1>
<p class="lead">
Now you can manage users, roles, and more with
our easy tools.
</p>
<p>
<?php
if (!Yii::$app->user->isGuest && $is_admin) {
echo Html::a('Manage Users', ['user/index'],
['class' => 'btn btn-lg btn-success']);
}
?>
</p>
</div>
<div class="body-content">
<div class="row">
<div class="col-lg-4">
<h2>Users</h2>
<p>
This is the place to manage users. You can edit status and roles from here.
The UI is easy to use and intuitive, just click the link below to get started.
</p>
<p>
<?php
if (!Yii::$app->user->isGuest && $is_admin) {
echo Html::a('Manage Users', ['user/index'],
['class' => 'btn btn-default']);
}
?>
297
298
</p>
</div>
<div class="col-lg-4">
<h2>Roles</h2>
<p>
This is where you manage Roles.
<p>
<?php
if (!Yii::$app->user->isGuest && $is_admin) {
echo Html::a('Manage Profiles', ['profile/index'],
['class' => 'btn btn-default']);
}
?>
</p>
</div>
</div>
<div class="row">
<div class="col-lg-4">
<h2>User Types</h2>
<p>
This is the place to manage user types. You can edit user
types from here. The UI is easy to use and intuitive, just
click the link below to get started.
</p>
<p>
<?php
if (!Yii::$app->user->isGuest && $is_admin) {
echo Html::a('Manage User Types', ['user-type/index'],
['class' => 'btn btn-default']);
}
299
?>
</p>
</div>
<div class="col-lg-4">
<h2>Statuses</h2>
<p>
This is where you manage Statuses. You can add or delete.
You can add a new status if you like, just click the link
below to get started.
</p>
<p>
<?php
if (!Yii::$app->user->isGuest && $is_admin) {
echo Html::a('Manage Statuses', ['status/index'],
['class' => 'btn btn-default']);
}
?>
</p>
</div>
<div class="col-lg-4">
<h2>Placeholder</h2>
<p>
Need to review Profiles? This is the place to get it done.
These are easy to manage via UI. Just click the link below
300
301
to manage profiles.
</p>
<p>
<?php
if (!Yii::$app->user->isGuest && $is_admin) {
echo Html::a('Manage Profiles', ['profile/index'],
['class' => 'btn btn-default']);
}
?>
</p>
</div>
</div>
</div>
</div>
The helpers class lets us format the urls with the following:
echo Html::a('Manage Roles', ['role/index'], ['class' => 'btn btn-default']);
Like we did in the last chapter,we are using the a method of the Html class.
Since we want to be consistent, we have wrapped each link in our If statement, to test if the user is
in fact admin or greater:
302
Like we did with the previous page, we set the $is_admin variable near the top of the file under the
title variable:
$is_admin = PermissionHelpers::requireMinimumRole('Admin');
This is not a complete security solution because if someone had somehow logged into the backend
without having a role of Admin or greater, they could still type in the url, so we will have to add
logic to the controllers as well.
Controller Behaviors
We will do this now, through the behaviors method on backend/controllers/SiteController.php.
As we have shown in a previous chapter, Yii 2 provides a matchCallback parameter on rules in
Access:Control and it works perfectly for our purposes. Lets replace the existing behaviors method
(dont forget the use statement for PermissionHelpers) with the following:
Gist:
Backend Site Controller Behaviors
From book:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'actions' => ['login', 'error'],
'allow' => true,
],
[
'actions' => ['index'],
'allow' => true,
'roles' => ['@'],
'matchCallback' => function ($rule, $action) {
303
return PermissionHelpers::requireMinimumRole('Admin')
&& PermissionHelpers::requireStatus('Active');
}
],
[
'actions' => ['logout'],
'allow' => true,
'roles' => ['@'],
],
],
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'logout' => ['post'],
],
],
];
}
Like I mentioned above, dont forget to add the use statement at the top of the file:
use common\models\PermissionHelpers;
Match Callback
We added the matchCallback rule to cover conditions where a users status or access level changes
after they have logged in. If you have to drop someones access level, you dont want them to have
any residual access that they shouldnt have.
We need to make changes to our other controllers too, and we will explain the rules in detail after
the changes are done. The backend controllers for User, UserType, Status, Profile, and Role are a
little different from the previous example and need the following code added under the behaviors
method:
Gist:
Backend Behaviors For All Other Controllers
From book:
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
304
305
];
}
Make sure that behavior method is in place for each of the controllers mentioned above, and once
again dont forget the use statement for PermissionHelpers:
use common\models\PermissionHelpers;
if that evaluates to false, we dont match and we get a You do not have permission to view this
page response. This is another reuse of the PermisssionHelpers method, which means we are getting
some good code reuse out of it. The fact that we made it a public static function means we can call
it anywhere we need it, as long as we include:
use common\models\PermissionHelpers;
So with our controller methods in place, we have a decent amount of security to stop someone who
is not admin from accessing admin processes. As we thought it through, we realized we needed
to check for status as well. What if someones status is downgraded during an open session? They
would still have access to the controller methods because status was only checked when they logged
in. This is important on the frontend too. So, for example, if you built a site for example, where users
could cancel their account, you would not want them to get around to areas of the site that required
active status.
Notice that we are checking to see if the users status_id is equal to the Active status.
We were supposed to change the behaviors in the frontend in a previous chapter,so if you have not
please go back and do so now.
Ok, finally we have our working back end. Now we can check out our new index page for admin
when we are logged in:
306
Summary
Commit!
Well, thats it. You now have an extended template made from Yii 2s advanced template. We covered
a lot of ground with this book, enough to get you up and running, which was our goal. You should
be able to use the template from this book to start your own projects.
307
This book was a starter book, so this really is only the beginning for you on your journey with Yii
2. To continue the journey, you should consult the guide and the forums for more information on
how to use the framework.
Please note I have already added bonus chapters and I will be updating the book to keep up with
changes and to add new material, so look for that when you can. All updates are free to anyone who
has purchased this book for the life of the book. I hope its a long life.
If you like this book, please recommend it to friends. You can visit my blog and leave feedback. Any
positive comments, links, and reviews are greatly appreciated.
Thanks for taking this journey with me. I hope to see you soon.
AutoResponder
Often a client will have a need for an autoresponder somewhere in their application. It could be
registration, sending in a support request, any of a number of things. And inevitably, when they
want an autoresponder, two things will happen:
1. They will want to revise the text.
2. They will think of more autoresponders they want in the future.
So what we need is a solution we can implement that scales easily to the clients needs. And this can
save you a lot of headaches too.
Imagine the client wants to change a single word in the text of their autoresponder and they want
you to push a new version of the site for it, and to top it off, they need that done over the weekend,
or civilization as we know it will come to an end.
The client gets over-excited, yet at the same time, they are paying the bills, so we have to listen. Its
a nightmare. Weve all been the there.
So I thought about how I could avoid that scenario completely. Wouldnt it be great if they could
just enter what they need into an admin page in the backend and never even bother to call me?
Ok, on one hand, when they call, we get paid, so we dont want them to stop calling us. On the other
hand, the more power we give them over their project, the more they will love us for the work we are
doing and the more they will come back for more, especially an enthusiastic client who appreciates
the attention to detail.
So obviously, if we were going to let the client update the text for the email via UI, we would most
likely be storing that text in our DB. And we know writing that UI with Giis auto-generated code
309
is a snap. So its not hard to imagine that part coming together quickly. We just need a simple data
structure and that will work nicely.
What else would we need? Well, we need a model that looks up the record and fires off the email.
And then here comes the buzz kill. How does it know which email to send and where do we put the
code that calls the appropriate method?
My first thought was to put this on a behavior on the controller, but that wasnt quite right. The
implementation of behaviors that Ive seen in controllers allow you to pick specific actions, which
is good, but defeats the purpose of applying to all actions, which is what I wanted.
If only there were a way in Yii 2 to apply a set of instructions to all actions. Actually there is. Its
called an afterAction. They also have a beforeAction method, but thats not what we were looking
for. So the afterAction method looks like this:
public function afterAction($action, $result)
{
//your code here
return parent::afterAction($action, $result);
}
It automatically takes in two arguments and as long as you call the parent, it will run after each
action on the controller.
So I thought, hmm, maybe this will work. Thinking out the code before writing it, I thought that we
could simply do a check on the action and controller name, see if there is a record for that specific
action/controller pair, and if so, return it and run the afterAction, which would have a method that
would send the email. Or if not, dont run the parent and return false. Or something like that.
So, eager to try this, I set up my data structure for the email messages. And because Im thinking a
little more ahead, I named the table status_message. I figured I might be able to reuse the body of
the emails for other messages at some point, and I wanted to give myself flexibility for extensibility.
So thats why I didnt call it email_message.
Also, note I used the singular. When choosing a name of a table, I try to stick with the convention
set by Yii 2, using the User table as an example. So I always go singular.
Feel free, however, to name it how you wish. Just make sure if you do pick a different name, to
reference it correctly in the rest of the tutorial.
So the table structure looks like this:
310
Synchronize the model the model to the DB or if you are just using Php MyAdmin, create the table
with those fields and constraints.
Also, note, as I stated in the beginning of the book, I do not use migrations, that is a personal choice,
but that is why I dont provide the migration. I use MySql Workbench and PhpMyAdmin for these
kinds of tasks. If you are following along and dont use either of those two, feel free to use the
method/tool of your choice.
It should look like this when you are done:
311
Ok, nothing too crazy there. I gave myself a description field, so I could describe the purpose of the
message.
Now this data structure may evolve over time, but to get things up and running, Ive kept it simple.
So after creating the data structure, I could see that I needed a simple check to see if a record existed,
and if so, return either false, indicating there is no message for that controller/action or return the id
of the message, which I could then use in another method to retrieve the parts of the message that I
wish to send via email.
Could I have done it all in one method? Yes, very easily, but by breaking it apart into multiple parts,
the code is easier to digest, easier to write, and easier to reuse.
We already have the perfect place to put this method, in our RecordHelpers class, located in
common/models/RecordHelpers.php.
First add the use statement at the top that will give us access to the model via ActiveRecord:
use backend\models\StatusMessage;
312
StatusMessage::find('id')
->where(['action_name' => $action_name])
->andWhere(['controller_name' => $controller_name])
->one();
As you can see, it does a simple lookup via ActionRecord, when you hand in the action_name and
controller name. Obviously we have a second parameter and we used andWhere() for that.
We use a ternary to see if we have a result, if were good, return the result. If not return false. The
result will give us the id of the record which we will hand into other methods. Very simple stuff.
Here are the other two methods that retrieve the message subject and message body, respectively.
Lets add them now to RecordHelpers.php:
Gist:
GetMessageSubject and GetMessageBody
From book:
public static function getMessageSubject($id)
{
$result =
StatusMessage::find('subject')
->where(['id' => $id])
->one();
return isset($result['subject']) ?
$result['subject'] :
}
public static function getMessageBody($id)
{
$result =
StatusMessage::find('body')
->where(['id' => $id])
->one();
false;
313
In both cases, we give the message_id that we want and we get back the message subject or body
that we need for the email. Later we will attach these results to our sendTheMail method, which
will be part of our MailCall class that we are going to create. Dont worry it sounds much more
complicated than it actually is, its very simple stuff.
Ok, back to our new methods. Note that we are using public static methods. This makes it easy to
place the method inside another method without having to use the longer instantiation syntax. You
will see what I mean in a minute or two.
Anyway, so now we have the record helpers that will help us return the data from the table.
You can see we are working our way backwards from the data. Obviously, we will need a mail
method to send the mail, once we have the bits of the message we need.
I needed to figure out how Yii 2 sends mail, so, remembering that there is a contact page on the
advanced template that sends a mail message, I used it as a guide for what I wanted to build. I only
mention this to point out that you can use a lot of what Yii 2 hands you in its templates as a guide
for what you want to build.
After studying the ContactForm model, I came up with my mail method. Lets create a new model
for this named MailCall. Go ahead and create MailCall.php inside of common/models.
Create an empty MailCall class with the following namespace :
namespace common\models;
314
Ok, so Yii 2 has a mailer class, accessed from the application instance Yii::$app. Now that should set
off a little reminder for you to include a use statement for Yii, otherwise, we are not going to have
access to Yii::
So below namespace add:
use yii;
Ok, you can see we have a whole bunch of methods chained together. And so now we see the logic
behind what we were doing if it was not already clear. Mailers setTo method takes a parameter
of the current users email address. For now we are hard-coding the from address and name. For
setSubject, we use our handy static call to:
RecordHelpers::getMessageSubject($message_id)
So again, that should indicate that we will use RecordHelpers, so we need to pull that in as well.
Add the following use statement:
use common\models\RecordHelpers;
This is all nice concise code and for the message body, same type of thing:
RecordHelpers::getMessageBody($message_id)
You can see that the method takes $message_id as a parameter, so now all we need is a method that
will call sendTheMail, that has the ability to hand in the $message_id.
So Now I go back to the beginning question because before I build the method, I need to know how
Im going to call it.
I was very happy with something like:
public function afterAction($action, $result)
{
MailCall::isMailable($action->id, getUniqueId());
return parent::afterAction($action, $result);
}
The plan was to create a method named isMailable on my newly created MailCall class. Since Im
using a static method, I can just pop it in there. I do that a lot for these helper methods.
$action->id returns action, so if this were the site controller and the index action were being called,
it would return index. And getUniqueId() returns the controller name. So that gave me the two
things I need to look up a record, see if it existed, if so, return the message id, and hand it into the
sendTheMail method.
So I was pretty happy. So much so, I took a break and went for a walk. And as I was enjoying the
nice cool ocean air, clearing my head, I realized I was making a mistake.
315
The method afterAction fires after every action, which is what I wanted, but it has no way to know
what I intended the success of the action to be. For example, lets say you had an action that says
save a record or show form for input, which we see a lot in our controllers. The afterAction method
will fire in both cases, no matter what, because as far as it is concerned, it has called the action. But
you would only want to send an email in certain cases, saving for example, not showing the form,
so using afterAction just went out the window.
And so it goes in programming. I needed a more discrete way of determining what would trigger
the email, and since that could vary greatly from action to action, the only way to do it correctly is
to place the method call inside of the action at precisely the point where I want it to execute.
So I thought again about my original idea, which was to anticipate the clients needs and give them
control over the content of the mail messages sent from different actions. The way I saw it, I had a
choice, for one way, I could embed the method call to send the mail only when explicitly asked to
do so by the client.
Because I built out a fairly robust way of storing the messages and sending them, it would become a
very trivial matter for me to add more locations on request by the client, who could them edit them
as they wished from the UI. This would be the standard approach.
Or, as an alternative, I could just embed the call wherever I thought it might be needed in the future,
since I was planning to have it test for the existence of the message and return false otherwise.
Of course I wouldnt throw it in everywhere, just in spots likely to require an autoresponder. So then
the question becomes is the DB overhead worth it. The extra call wherever I place it would slow
down the site a tiny bit.
To give a more concrete example of what Im talking about. Lets say the client wants an
autoresponder on registration. The user registers and gets an email confirming it. Our class and
methods, which we have not completed yet, but will shortly, will handle this beautifully. Perfect.
But looking over our template, we see that we have a contact form and wouldnt it be cool to be able
to send an auto-response whenever someone contacts us? This is standard functionality on most
sites and the client might not realize it yet, but they will probably want this too.
So as a compromise, we could embed the method calls to send mail everywhere we think the client
will request them during development, then, after a final review with the client, remove the unused
ones from the code. This creates a little cleanup work, but would actually be very simple to do.
This way, during site development, when the client begs you to add an autoresponder, you can tell
them to just add the DB record via backend UI, with no additional coding required. This would keep
them happy and you would be one step ahead.
Of course the level of implementation is up to you. I only mention all this because you can see that
as a project progresses, things tend to change. I wanted to build a behavioral method that checked
every action, but that turned out not to be realistic, so I compromised and settled for an inline action
method instead.
Ok, so lets look at what we have so far.
316
if (Yii::$app->getUser()->login($user)) {
return $this->goHome();
}
}
}
return $this->render('signup', [
'model' => $model,
]);
}
Nothing special here, this is what you get out of the box on the advanced template. So we need to
add something like:
MailCall::onMailableAction('signup', 'site');
That seems concise and easy to understand. I really try to boil it down to as little as possible and
yet still be intuitive. MailCall is the name of the class we created for the sendTheMail method. The
onMailableAction method, we have not created yet. But we can see that it takes the action and
controller names as arguments, which are also names of fields in our DB.
We will use the action name and controller name to find the specific message we want to mail out
for this action.
Its worth noting that I thought of another way to reference the arguments:
317
MailCall::onMailableAction(__METHOD__, getUniqueId());
The syntax on METHOD is a magic method to return the name of the current method and like I
said before, getUniqueId() returns the name of the controller. So that would get us, actionSignup
and site, which is not quite right. So in order to use this approach, we would have to get rid of the
word action.
So I did this, rather incorrectly, as the following:
$method_name = str_replace("action","",$method_name);
$action_name = $method_name;
Cool, it strips out action and replaces it with nothing. Yeah, but what happens if you have an action
named actionTraction? It will strip action out of traction and return an error. Obviously I could just
remove the first six characters from the string, which would be the correct way to do it. But I never
bothered to write it that way, heres why.
I didnt want to have to remember what the magic METHOD and getUniqueId() were returning.
Also, if someone else had to maintain the code, they would not be able to instantly understand what
was happening. Plus, to explicitly name the controller and action instead actually required less code.
Sometimes it just comes down to a preference of what you want to stare at. You will spend far more
time reading code than writing it. So the easier that code is to read and get at first glance, the better.
Now in terms of naming the method, I use onMailableAction because it is slightly ambiguous, which
I think is appropriate because nothing is going to happen if there is no corresponding message in
the DB.
Ok, so lets provide the last method on MailCall to wrap this up. Add this to your MailCall class.
Gist:
OnMailableAction
From book:
public static function onMailableAction($action_name, $controller_name)
{
if ($message_id = RecordHelpers::findStatusMessage
($action_name, $controller_name)){
static::sendTheMail($message_id);
}
}
318
Obviously the if statement is broken into two lines because of wordwrap. You should use it as one
line in your IDE.
Its so simple. It hands in the action name and controller name, then uses our helper method to
try to get a record in the DB and set it to $message_id. If it cant set $message_id, then it never
calls MailCall. If it can set $message_id correctly, then it calls the sendTheMail method with the
$messgae_id handed in.
For the sake of consistency, I will provide the entire MailCall.php file.
Gist:
MailCall.php
From book:
<?php
namespace common\models;
use yii;
use common\models\RecordHelpers;
class MailCall
{
public static function sendTheMail($message_id)
{
return Yii::$app->mailer->compose()
->setTo(\Yii::$app->user->identity->email)
->setFrom(['[email protected]' => 'Yii 2 Build'])
->setSubject(RecordHelpers::getMessageSubject($message_id))
->setTextBody(RecordHelpers::getMessageBody($message_id))
->send();
}
public static function onMailableAction($action_name, $controller_name)
{
if ($message_id = RecordHelpers::findStatusMessage
($action_name, $controller_name)){
static::sendTheMail($message_id);
}
319
}
}
Again the if statement is broken into two lines because of wordwrap. You should use it as one line
in your IDE.
So to call this correctly in our signup action on the site controller:
Gist:
Action Signup
From book:
public function actionSignup()
{
$model = new SignupForm();
if ($model->load(Yii::$app->request->post())) {
if ($user = $model->signup()) {
if (Yii::$app->getUser()->login($user)) {
MailCall::onMailableAction('signup', 'site');
return $this->goHome();
}
}
}
return $this->render('signup', [
'model' => $model,
]);
}
You see that we popped in our onMailableAction method right after the user is logged in. This is
because, as you recall from our sendTheMail method, we are using:
->setTo(\Yii::$app->user->identity->email)
And you only have access to Yii::$app->user->identity->email if the user is logged in.
Also note, this is now one line of code in the controller, everything else is extracted out into other
classes. How awesome is that? Nice clean controller code.
Just dont forget to pull MailCall into SiteController:
320
use common\models\MailCall;
At this point, since we have no records in our DB, nothing should happen, except normal registration
of a user. However you should register a user and see if it is throwing any errors.
Now that weve tested ok so far, lets use Gii to build out our UI in the backend, then we can add
some records and play with this.
We can move quickly on this, since were so used to Gii at this point, and this is where you see just
how awesome that code generation is for building code quickly.
Ok, first step, build the model. I wont provide a screenshot because you should know how to do
this by now. I will mention, however, that my choice is to put the model in the backend, that seems
logical to me.
I know Ive mentioned it before, that Yii 2 recommends putting models in common and then
extending them in frontend and backend, but typically that is not my personal choice. And you
can see as we add models to common, how handy it is to have that folder reserved for helpers and
other models.
Ok, so Im going to assume that built your status_message model, and then built your CRUD from
StatusMessage. Im sure I dont have to point out how amazing Gii is at this point, but I will
anyway. 8 files are being created for you that will only require minor changes. Not only do you
get standardization, but you pick up an amazing amount of efficiency in both time and effort.
So lets move through some minor changes. Lets add our timestamp behavior to the model,
StatusMessage.php:
Gist:
StatusMessage Behaviors
From book:
public function behaviors()
{
return [
'timestamp' => [
'class' => 'yii\behaviors\TimestampBehavior',
'attributes' => [
ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
],
'value' => new Expression('NOW()'),
321
],
];
}
And of course you know that means you will have to pull in:
use yii\db\ActiveRecord;
To be consistent, lets make the behaviors in our StatusMessageController the same as for our other
admin controllers, such as role. So replace the behaviors on the StatusMessageController with the
following:
Gist:
StatusMessageController
From book:
public function behaviors()
{
return [
'access' => [
'class' => \yii\filters\AccessControl::className(),
'only' => ['index', 'view','create', 'update', 'delete'],
'rules' => [
[
'actions' => ['index', 'create', 'update', 'view',],
'allow' => true,
'roles' => ['@'],
'matchCallback' => function ($rule, $action) {
return PermissionHelpers::requireMinimumRole('Admin')
&& PermissionHelpers::requireStatus('Active');
}
],
[
'actions' => [ 'delete'],
'allow' => true,
322
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
];
}
One difference you can see is that I moved action update to admin level access. And, dont forget to
pull in:
use common\models\PermissionHelpers;
Now we can work on the views. You can change a textInput field on backend/views/statusmessage/_form.php to the following:
<?= $form->field($model, 'body')->
textArea(['maxlength' => 2025, 'rows' =>12]) ?>
Two lines to avoid the dreaded word-wrapping again. Use this as one line in your IDE.
So we changed it from textInput to textArea and also handed in the optional rows parameter, which
lets you control the size of the text box. I also moved status_message_description and put it directly
under status_message_name because that seems like a more logical order of fields.
323
And obviously we can get rid of created_at and updated_at fields, since we have behaviors handling
those.
And last step, we need to add navigation to our views. Lets go to backend/views/layouts/main.php
and add one more menu item:
$menuItems[] = ['label' => 'Status Messages', 'url' => ['status-message/index']];
For UI purposes, I made the label plural. Note the way the controller is referenced. Putting a dash
between the two words is the convention here, even though the name of the controller file is
StatusMessageController.php.
This UI is starting to get silly with so many items running across the nav. Dont worry, switching
to drop down nav is our next section of bonus material, so we will be changing it.
Ok right now, if you save this, you can get to all the views and test everything. I will provide a
screenshot of my first record, just to make sure we got it all right and understand what the fields
mean, since we covered that a while back.
324
We start with the controller name. Then the action name, case sensitive, dont include the word
action. Next comes the name of the status message, followed by a brief description. Then we have
the subject and body. So go ahead and save this record.
Next, register a new user. Then check your forntend/runtime/mail folder and you will see your email
in there.
Runtime Mail
I know I mentioned it before, but this is where all emails go in dev mode. Check that and youll see
that it indeed did send an email, with exactly the right content.
Now anywhere you want an autorepsonder in the application, just pop into the controller:
MailCall::onMailableAction($action_name, $controller_name);
And obviously, you have to use strings for the method signature, not variables. So just to reiterate
for clarity, if you wanted to put that somewhere in the site controllers contact action:
MailCall::onMailableAction('contact', 'site');
325
If you didnt already have the use statement for MailCall included, you would have to include that
as well. Then just create a matching status message record and it works. You can see how easy this
will be to implement across the application.
And thats pretty much it. As with all code, it is not meant to be a final solution or grand solution,
just one possibility among many. A lot of the choices you make in coding come down to personal
preference and there is typically always room for improvement. Just do it your way and have fun
coding!
Dropdown Navigation
Its time we used dropdown navigation for our top nav in the backend, otherwise we end up with
way too much clutter. This should be the easiest task we face, right?
Unfortunately, its not clear from the Yii 2 guide how their dropdown widget works, at least not
to me. I wasnt satisfied with not knowing, so I looked around and googled like crazy. I saw an
implementation on someone elses plugin and figured that the native Yii 2 Nav widget might work
the same way, so I figured out a solution.
The actual implementation looks like this:
echo Nav::widget([
'options' => ['class' => 'navbar-nav navbar-right'],
'items' => [
['label' =>
['label'
['label'
['label'
]],
['label' => 'Support', 'items' => [
['label' => 'Support Requests', 'url' => ['/content/index']],
['label' => 'Status Messages', 'url' => ['/status-message/index']],
['label' => 'FAQ', 'url' => ['/faq/index']],
['label' => 'FAQ Category', 'url' => ['/faq-category/index']],
]],
['label' => 'RBAC', 'items' => [
['label' => 'Roles', 'url' => ['/role/index']],
['label' => 'User Types', 'url' => ['/user-type/index']],
326
]],
],
]);
I purposely did not provide the Gist. I will be giving you the entire file when Im done explaining
everything. You can see from the above, Ive simply used nested arrays to create the dropdown.
As you know from previous chapters, Ive wrapped the admin portions of the navigation in an if
statement to determine if the user is logged in and has a minimum of admin status:
if (!Yii::$app->user->isGuest
&&
$is_admin) {
So you would think I could just pop this into place where I previously had the nav, but no. We have
to put the test for logged in or out above it:
if (Yii::$app->user->isGuest) {
$menuItemsLogOut[] = ['label' => 'Login', 'url' => ['site/login']];
} else {
$menuItemsLogOut[] =
['label' => 'Logout (' . Yii::$app->user->identity->username . ')',
'url' => ['/site/logout'],
'linkOptions' => ['data-method' => 'post']
];
}
327
echo Nav::widget([
'options' => ['class' => 'navbar-nav navbar-right'],
'items' => $menuItemsLogOut
]);
It pulls everything to the right. So each subsequent Nav::widget stacks to the left of the first one. So
essentially they need to go in reverse order.
Also, you can see that Ive used two different $menuItems arrays. This is because we now have
multiple Nav::Widget calls, so we cant use the same $menuItems array for both of them.
In this if statement:
if (Yii::$app->user->isGuest) {
$menuItemsLogOut[] = ['label' => 'Login', 'url' => ['site/login']];
} else {
$menuItemsLogOut[] =
['label' => 'Logout (' . Yii::$app->user->identity->username . ')',
'url' => ['/site/logout'],
'linkOptions' => ['data-method' => 'post']
];
}
echo Nav::widget([
'options' => ['class' => 'navbar-nav navbar-right'],
'items' => $menuItemsLogOut
);
First we are setting an element to the array, depending on whether or not the user is logged in. Then
we call the array from within Nav::Widget.
This is a useful technique for testing for the state of the user, then deciding what to show them. Note
that on the if statement where we decide whether or not to show the items at all, we dont need
separate arrays because its a choice between showing the items or nothing.
328
Like I said, this can be tricky to work with. Im going to give the entire backend/views/layouts/main.php file, so we can make sure we have everything in the correct order:
Gist:
Backend Layouts Main
from book:
<?php
use
use
use
use
use
use
use
backend\assets\AppAsset;
yii\helpers\Html;
yii\bootstrap\Nav;
yii\bootstrap\NavBar;
yii\widgets\Breadcrumbs;
common\models\PermissionHelpers;
backend\assets\FontAwesomeAsset;
/**
* @var \yii\web\View $this
* @var string $content
*/
AppAsset::register($this);
FontAwesomeAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
<meta charset="<?= Yii::$app->charset ?>"/>
<meta name="viewport"
content="width=device-width,
initial-scale=1">
<?= Html::csrfMetaTags() ?>
<?php
if (!Yii::$app->user->isGuest){
$is_admin = PermissionHelpers::requireMinimumRole('Admin');
NavBar::begin([
'brandLabel' => 'Yii 2 Built <i class="fa fa-plug"></i> Admin',
'brandUrl' => Yii::$app->homeUrl,
'options' => [
'class' => 'navbar-inverse navbar-fixed-top',
],
]);
} else {
NavBar::begin([
'brandLabel' => 'Yii 2 Built <i class="fa fa-plug"></i>',
'brandUrl' => Yii::$app->homeUrl,
'options' => [
'class' => 'navbar-inverse navbar-fixed-top',
],
]);
}
if (Yii::$app->user->isGuest) {
329
330
$is_admin) {
echo Nav::widget([
'options' => ['class' => 'navbar-nav navbar-right'],
'items' => [
['label' => 'Users', 'items' => [
['label' => 'Users', 'url' => ['user/index']],
['label' => 'Profiles', 'url' => ['profile/index']],
['label' => 'Something else here', 'url' => ['#']],
]],
['label' => 'Support', 'items' => [
['label' => 'Support Requests', 'url' => ['content/index']],
['label' => 'Status Messages', 'url' => ['status-message/index']],
['label' => 'FAQ', 'url' => ['faq/index']],
['label' => 'FAQ Categories', 'url' => ['faq-category/index']],
]],
['label' => 'RBAC', 'items' => [
['label' => 'Roles', 'url' => ['role/index']],
['label' => 'User Types', 'url' => ['user-type/index']],
['label' => 'Statuses', 'url' => ['status/index']],
]],
NavBar::end();
?>
<div class="container">
<?= Breadcrumbs::widget([
'links' => isset($this->params['breadcrumbs']) ?
$this->params['breadcrumbs'] : [],
])?>
<?= $content ?>
</div>
</div>
331
332
<footer class="footer">
<div class="container">
<p class="pull-left">© Yii 2 Build <?= date('Y') ?></p>
<p class="pull-right"><?= Yii::powered() ?></p>
</div>
</footer>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>
Something you may have noticed in the above file. We dont echo NavBar::begin. We do, however,
need to echo the nav elements.
Also note, I have put some placeholders in certain spots in the dropdown for demonstration purposes.
When you play around with this, youll probably notice the cool highlight it gives for the selected
dropdown. At first I thought that was a CSS mistake until I realized it was highlighting the dropdown
element for the page we were on. When you are done, it should look like the image below:
Dropdown Nav
Well, that its for the Nav dropdown, I hope you find that useful.
333
FAQ
Next were going to set up an FAQ model and use some interesting elements that we have not used
before, including use of Yii 2s ArrayDataProvider, a very handy iterator for use in sending data to
the views.
When we think of FAQs, we think of simplicity, just questions and answers, and simplicity typically
serves us well. But if we think about our demanding client and things that are important to them,
the order of the questions, for example, might be very important. And obviously, the order they
want will most likely not match the ascending or descending timestamp for creation or alphabetical
order.
In most cases, the client wont even know what order they want the questions in until long after the
project has been developed. So what we need is a way for them to determine the order via backend
UI. Now this is really simple to do of course. We just add a faq_value column, with a data type of
int, that we can sort either ascending or descending depending on our preference.
But thinking it over, faq_value as a name for the column might be too generic. I could call it
something more descriptive like faq_importance or faq_weight. I think faq_importance is too much
to type and prone to typos, but faq_weight seems like a good choice.
The reason why I just dont call it weight, is that I might use the weight concept on another table
and I try to avoid having the same column name in different tables as much as I can. This helps
avoid ambiguation problems with queries later on.
So the way our faq_weight would work would be that in an ascending sort, an FAQ with a faq_weight of 10 will be higher on the list than an FAQ with an faq_weight of 20. And as long as we
give the client a method for changing the faq_weight of an FAQ from backend UI, the problem of
the client needing to control the order is anticipated and solved before it ever becomes an issue.
So thinking along the same lines, what if the client wants to grab a group of specific questions and
present them on different parts of the application? We can anticipate that they may well want to
do that, so we can create a boolean field on the db for faq_is_featured. That way if we need to call
a group of featured FAQs for special presentation, we have an easy way to extract those FAQs, we
just call FAQs where faq_is_featured is set to yes, in other words, a 1.
So lets get started and set up the data structure for FAQ. We will need the following columns on
our faq table:
id (int, PK, NN, AI)
faq_question (VARCHAR(255), NN)
faq_answer (VARCHAR(1055), NN)
faq_category_id (INT)
faq_is_featured (BOOL, DEFAULT = 0)
faq_weight (INT, DEFAULT = 100)
334
created_by (INT)
updated_by (INT)
created_at (DATETIME)
updated_at (DATETIME)
Here is a screenshot:
faq table
faq table
335
Ok, so you can see that Ive set up a separate faq_category table to hold the names of the categories.
And just to be one step ahead of the game, Ive built into the data structure the same type of ordering
that I intend for faq. While we wont actually be using it for this chapter, it is there, should you decide
to implement it.
If our pesky client falls in love with ordering his FAQs, he or she might just demand the same control
over faq_categories.
The results in Php Myadmin should look like this:
faq table
336
Something else you may have noticed in our data structure are the columns of created_by and
updated_by on our faq table. These columns will allow us to implement a blameable behavior on
the model that will automatically stamp it with the user who is creating or updating the record.
Dont you just love the name blameable?
Sometimes its important to know who created a record, theres a variety of reasons for this. It could
be that we dont understand why it exists, and so we have to ask the team member that created it.
That would be a backend scenario.
On the frontend, if someone posts something that doesnt otherwise visibly track the user doing it,
this is a way to track the user, which can be useful for security reasons.
So those are a couple of scenarios where you would want to know who the user is, and obviously
there are a lot more. Blameable is a really handy behavior that Yii 2 provides for us, so we will
implement it here, and then you can decide where on the rest of the application you would like to
use it.
Another thing were going to do differently here, is take a backend model and create a separate
frontend UI for it, this time utilizing the ArrayDataProvider that we have not used yet.
Lets go ahead and begin by using Gii to create the two models, Faq and FaqCategory, which are
based on the two tables we just made. We will build these models into the backend, so make sure
the namespace is for backend\models.
Im not going to provide screenshots for this because by now you should know how to use Gii. If
you need a refresher, please refer to one of the earlier chapters.
Now lets make the crud for both models. Remember we dont need to supply a view path, we are
using the default.
Again, Im not going to provide screenshots for this because by now you should know how to create
the CRUD in Gii. If you need a refresher, please refer to one of the earlier chapters.
For each model, you should have a file for:
model
search model
controller
view
update
index
create
_search
_form
Go ahead and check your folders in backend and make sure you have everything. If all is not good,
retrace your steps to figure out what went wrong. If the files are missing, and yet you generated
337
them, they most likely went to the wrong place, which can happen if you make an error in the
namespace entries.
At this point, I will assume all is good and move on to modifying files. So we need to make some
changes, and well start with the model FaqCategory.php first.
Since I used a foreign key in MySql Workbench to create the table relationship between Faq and
FaqCategory, Gii has automatically made the necessary relationship method. I will supply it here in
case you skipped the foreign key:
Gist:
Get Faqs
From book:
public function getFaqs()
{
return $this->hasMany(Faq::className(), ['faq_category_id' => 'id']);
}
The next thing we want to add to our model is a simple method to return the dropdown list options
for faq_category_is_featured. Well need this for our form view, so we can return yes or no in a
dropdown list instead of having to enter 0 or 1 into a form field.
Now we covered this previously when the dropdown list was created from values in a related model,
but this is the first time we are doing it this way. This is just formatting the data for the view, no
relationship is necessary. But we are doing it in a way that is very consistent with how we do our
other methods by placing it on the model, as opposed to inline in the form.
Anyway, here is the method:
Gist:
GetFaqCategoryIsFeaturedList
From book:
public static function getFaqCategoryIsFeaturedList()
{
return $droptions = [0 => "no", 1 => "yes"];
}
338
The Yii 2 guide shows an inline version of this in the form, but I prefer to create a reusable method.
It avoids code duplication because its very likely you could end up using this drop down list in more
than one place.
Also, another benefit is that when you are creating your relationships on your models, you get into
a habit of creating one for the dropdown list, which could typically be used in your rules, though
we are not doing that this time.
Speaking of rules, we also need to do a little work there, so we get our default set the way we want
it:
Gist:
FaqCategory Rules
From book:
public function rules()
{
return [
[['faq_category_name'], 'required'],
[['faq_category_weight', 'faq_category_is_featured'], 'integer'],
['faq_category_weight', 'default', 'value' => 100],
[['faq_category_weight'],'in', 'range'=>range(1,100)],
[['faq_category_name'], 'string', 'max' => 45]
];
}
We added a rule for default value to make sure that gets set, and we are enforcing a range for category
weight, from 1 to a 100, using built-in php function range.
Next, lets go to FaqCategoryController and change the behaviors method to make it similar to our
other controllers, but this time well just have one array for rules:
Gist:
FaqCategoryController
From book:
So now we can work on our views. Lets change the _form view first:
Gist:
FaqCategory Form View
From book:
339
340
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/* @var $this yii\web\View */
/* @var $model backend\models\FaqCategory */
/* @var $form yii\widgets\ActiveForm */
?>
<div class="faq-category-form">
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'faq_category_name')->
textInput(['maxlength' => 45]) ?>
<?= $form->field($model, 'faq_category_weight')->textInput() ?>
<?= $form->field($model, 'faq_category_is_featured')->
dropDownList($model->faqCategoryIsFeaturedList,
[ 'prompt' => 'Please Choose One' ])?>
<div class="form-group">
<?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update',
['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
So now that we have our form built, lets go ahead and add a record. Login to backend.yii2build.com
and use the content dropdown to find FAQ Category. Youll notice that in the nav its referring to
the singular, but on the page, its plural, so lets take a moment to fix the nav in main.php:
['label' => 'FAQ Categories', 'url' => ['faq-category/index']],
341
After making the change navigate back to backend.yii2build.com and select FAQ Categories from
the dropdown.
All the pages should work, even though we only modified _form.php. So from index.php, which is
where the nav took you, just click on the create button and you should see our modified form. Add
a couple of test records, you can use General and Specific as the test categories.
All the functions should work, including the defaults. Also, if you try entering a number greater
than 100 into the Faq Category Weight form field, you will see it returns an error message, so we
know our in range validator is working. Cool stuff.
Ok, so once you verified thats all working properly, lets punch our way through the rest of the
view changes.
On _search, its a one line change. We need to swap the text input for faq_category_is_featured
with:
<?= $form->field($model, 'faq_category_is_featured')->
dropDownList($model->faqCategoryIsFeaturedList,
[ 'prompt' => 'Please Choose One' ])?>
Note the second use of the faqCategoryIsFeaturedList method, so it didnt take long for that theory
to prove out.
Obviously the code is broken into 3 lines to avoid word wrap in PDF. You should make that a single
line in your file.
Ok, moving on. There are no changes at this point to create and update, so now we can tackle
view.php. Just a few minor changes:
Gist:
FaqCategory View.php
From book:
<?php
use yii\helpers\Html;
use yii\widgets\DetailView;
/* @var $this yii\web\View */
/* @var $model backend\models\FaqCategory */
$this->title = $model->faq_category_name;
$this->params['breadcrumbs'][] =
['label' => 'Faq Categories', 'url' => ['index']];
342
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="faq-category-view">
<h1>Faq Category: <?= Html::encode($this->title) ?></h1>
<p>
<?= Html::a('Update', ['update', 'id' => $model->id],
['class' => 'btn btn-primary']) ?>
<?= Html::a('Delete', ['delete', 'id' => $model->id], [
'class' => 'btn btn-danger',
'data' => [
'confirm' => 'Are you sure you want to delete this item?',
'method' => 'post',
],
]) ?>
</p>
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'faq_category_name',
'faq_category_weight',
['attribute'=>'faq_category_is_featured',
'format'=>'boolean'],
],
]) ?>
</div>
So 3 little changes here. We changed the title to $model->faq_category_name instead of just the id,
and we add the words Faq Category: to our h1.
The last change was to an attribute in the DetailView widget:
['attribute'=>'faq_category_is_featured', 'format'=>'boolean'],
Yii 2 allows us to set the format of the attribute, so in this case we set it to boolean, so now we get
Yes and No instead of 0 or 1 for the faq_category_is_featured output.
If everything went well, you view page should look like this:
echo Collapse::widget([
'items' => [
// equivalent to the above
343
344
[
'label' => 'Search',
'content' => $this->render('_search',
['model' => $searchModel]) ,
// open its content by default
//'contentOptions' => ['class' => 'in']
],
]
]);
?>
<p>
<?= Html::a('Create Faq Category', ['create'],
['class' => 'btn btn-success']) ?>
</p>
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'id',
'faq_category_name',
'faq_category_weight',
['attribute'=>'faq_category_is_featured',
'format'=>'boolean'],
['class' => 'yii\grid\ActionColumn'],
],
]); ?>
</div>
Three simple changes. We pull in the Collapse widget in the use statement, and call our _search
within the Collapse widget, just like we did on the other models. And then we formatted the boolean
for our attribute faq_category_is_featured, like we did on the view page. And that should look like
this:
345
FaqCategory Index
Ok, cool, so thats it for FaqCategory, all built, neat and tidy.
Now were ready to tackle the Faq model itself. Were going to start by adding the behaviors method,
which will also contain the blameable behavior.
Gist:
Faq behaviors
From book:
public function behaviors()
{
return [
'timestamp' => [
'class' => 'yii\behaviors\TimestampBehavior',
'attributes' => [
ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
],
'value' => new Expression('NOW()'),
],
'blameable' => [
'class' => BlameableBehavior::className(),
'createdByAttribute' => 'created_by',
'updatedByAttribute' => 'updated_by',
],
346
];
}
We need the following add to the use statement at the top of the file:
use yii\db\ActiveRecord;
use yii\db\Expression;
use yii\behaviors\BlameableBehavior;
You can see how we set up blameable in the behaviors array and this syntax is very straightforward.
All we have to do is map createdByAtrribute to created_by, which is the column name we used
in our data structure, and do the same for updatedByAttribute, and were set.
Next we have to add our changes for our rules.
Gist:
Faq rules
From book:
/**
* @inheritdoc
*/
public function rules()
{
return [
[['faq_question', 'faq_answer'], 'required'],
[['faq_category_id', 'faq_is_featured', 'faq_weight', 'created_by',
'updated_by'], 'integer'],
[['faq_weight'],'in', 'range'=>range(1,100)],
['faq_weight', 'default', 'value' => 100],
[['created_at', 'updated_at'], 'safe'],
[['faq_question'], 'string', 'max' => 255],
[['faq_question'], 'unique'],
[['faq_answer'], 'string', 'max' => 1055]
];
}
Notice we that besides in the range rule and default for faq_weight, we also have a unique rule for
faq_question. This works great and stops you from repeating the question in Faq.
Next, lets move on to our relationship and associated methods. We actually have quite a few for
such a simple model, but they will all come in very handy.
347
Gist:
Faq Relationships
From book:
/**
* usess magic getFaqCategoryName on return statement
*
*/
public function getFaqCategoryName()
{
return $this->faqCategory->faq_category_name;
}
/**
* get list of FaqCategory for dropdown
*/
public static function getFaqCategoryList()
{
$droptions = FaqCategory::find()->asArray()->all();
return Arrayhelper::map($droptions, 'id', 'faq_category_name');
}
public static function getFaqIsFeaturedList()
{
return $droptions = [0 => "no", 1 => "yes"];
}
public function getFaqIsFeaturedName()
{
return $this->faq_is_featured == 0 ?
"no" : "yes";
348
* @getCreateUserName
*
*/
public function getCreatedByUsername()
{
return $this->createdByUser ?
$this->createdByUser->username : '- no user -';
}
public function getUpdatedByUser()
{
return $this->hasOne(User::className(), ['id' => 'updated_by']);
}
/**
* @getUpdateUserName
*
*/
public function getUpdatedByUsername()
{
return $this->updatedByUser ?
$this->updatedByUser->username : '- no user -';
}
Since we have covered most of these previously, Im only going to talk about the last 4. When we
display created by and update by in our views, we need hook into the user table either by the
getCreatedByUser method or the getUpdatedByUser method.
Then we use the relationship to be able to return the username in the next method, getCreatedByUsername, which is what we will use in our views.
And of course to have access to the User and FaqCategory model, as well as the helper classes, we
need to add to our use statement.
Gist:
Use Statement
From book:
use
use
use
use
use
backend\models\FaqCategory;
yii\helpers\ArrayHelper;
yii\helpers\Url;
yii\helpers\Html;
common\models\User;
349
And lastly, we need to add to replace our attributeLabels with the following.
Gist:
Faq Labels
From book:
public function attributeLabels()
{
return [
'id' => 'ID',
'faq_question' => 'Question',
'faq_answer' => 'Answer',
'faq_category_id' => 'Category',
'faq_weight' => 'Weight',
'faq_is_featured' => 'Featured?',
'created_by' => 'Created By',
'updated_by' => 'Updated By',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'faqCategoryName' => Yii::t('app', 'Category'),
'faqCategoryList' => Yii::t('app', 'Category'),
'faqIsFeaturedName' => Yii::t('app', 'Featured'),
'createdByUserName' => Yii::t('app', 'Created By'),
'updatedByUserName' => Yii::t('app', 'Updated By'),
];
}
Just for reference, so you have the complete and correct Faq model, Im going to include the code
for the entire model here.
Gist:
Faq Model
From book:
<?php
namespace backend\models;
use
use
use
use
use
use
use
use
use
Yii;
yii\db\ActiveRecord;
yii\db\Expression;
yii\behaviors\BlameableBehavior;
backend\models\FaqCategory;
yii\helpers\ArrayHelper;
yii\helpers\Url;
yii\helpers\Html;
common\models\User;
/**
* This is the model class for table "faq".
*
* @property integer $id
* @property string $faq_question
* @property string $faq_answer
* @property integer $faq_category_id
* @property integer $faq_is_featured
* @property integer $faq_weight
* @property integer $created_by
* @property integer $updated_by
* @property string $created_at
* @property string $updated_at
*
* @property FaqCategory $faqCategory
*/
class Faq extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
{
return 'faq';
}
public function behaviors()
350
{
return [
'timestamp' => [
'class' => 'yii\behaviors\TimestampBehavior',
'attributes' => [
ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
],
'value' => new Expression('NOW()'),
],
'blameable' => [
'class' => BlameableBehavior::className(),
'createdByAttribute' => 'created_by',
'updatedByAttribute' => 'updated_by',
],
];
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['faq_question', 'faq_answer'], 'required'],
[['faq_category_id', 'faq_is_featured', 'faq_weight',
'created_by', 'updated_by'], 'integer'],
[['faq_weight'],'in', 'range'=>range(1,100)],
['faq_weight', 'default', 'value' => 100],
[['created_at', 'updated_at'], 'safe'],
[['faq_question'], 'string', 'max' => 255],
[['faq_question'], 'unique'],
[['faq_answer'], 'string', 'max' => 1055]
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
351
return [
'id' => 'ID',
'faq_question' => 'Question',
'faq_answer' => 'Answer',
'faq_category_id' => 'Category',
'faq_weight' => 'Weight',
'faq_is_featured' => 'Featured?',
'created_by' => 'Created By',
'updated_by' => 'Updated By',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'faqCategoryName' => Yii::t('app', 'Category'),
'faqCategoryList' => Yii::t('app', 'Category'),
'faqIsFeaturedName' => Yii::t('app', 'Featured'),
'createdByUserName' => Yii::t('app', 'Created By'),
'updatedByUserName' => Yii::t('app', 'Updated By'),
];
}
/**
* @return \yii\db\ActiveQuery
*/
public function getFaqCategory()
{
return $this->hasOne(FaqCategory::className(),
['id' => 'faq_category_id']);
}
/**
* usess magic getFaqCategoryName on return statement
*
*/
public function getFaqCategoryName()
{
return $this->faqCategory->faq_category_name;
}
/**
* get list of FaqCategory for dropdown
*/
352
353
"no" : "yes";
354
*/
public function getUpdatedByUsername()
{
return $this->updatedByUser ?
$this->updatedByUser->username : '- no user -';
}
}
And that does it for the Faq model. Now lets look at Faq Search. Im not going to step you through
the changes because this was covered in detail in chapter 11, how to modify the native search to use
eager loading relationships.
Even though the FaqCategories table is likely to be small, it doesnt hurt to use eager loading for
efficiency.
Gist:
Faq Search
From book:
<?php
namespace backend\models\search;
use
use
use
use
use
use
use
Yii;
yii\base\Model;
yii\data\ActiveDataProvider;
yii\data\ArrayDataProvider;
yii\db\ActiveQuery;
yii\db\Query;
backend\models\Faq;
/**
* FaqSearch represents the model behind the search form about `backend\models\F\
aq`.
*/
class FaqSearch extends Faq
{
public $faqCategoryName;
public $faqCategoryList;
public $faqIsFeaturedName;
public $createdByUsername;
public $updatedByUsername;
public $faq_category;
public $faq_weight;
/**
* @inheritdoc
*/
public function rules()
{
return [
[['id', 'faq_category_id', 'faq_weight', 'faq_is_featured',
'created_by', 'updated_by'], 'integer'],
[['faq_question', 'faq_answer', 'created_at', 'updated_at',
'faqCategoryName', 'faqCategoryList', 'faqIsFeaturedName',
'createdByUsername', 'updatedByUsername', 'faq_category',
'faq_weight'], 'safe'],
];
}
/**
* @inheritdoc
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = Faq::find();
$dataProvider = new ActiveDataProvider([
'query' => $query,
355
]);
/**
* Setup your sorting attributes
* Note: This is setup before the $this->load($params)
* statement below
*/
$dataProvider->setSort([
'defaultOrder' => [
'faq_weight' => SORT_ASC,
],
'attributes' => [
'id',
'faq_question' => [
'asc' => ['faq.faq_question' => SORT_ASC],
'desc' => ['faq.faq_question' => SORT_DESC],
'label' => 'Question'
],
'faq_answer' => [
'asc' => ['faq.faq_answer' => SORT_ASC],
'desc' => ['faq.faq_answer' => SORT_DESC],
'label' => 'Answer'
],
'faqCategoryName' => [
'asc' => ['faq_category.faq_category_name' => SORT_ASC],
'desc' => ['faq_category.faq_category_name' => SORT_DESC],
'label' => 'Category'
],
'faq_weight' => [
'asc' => ['faq.faq_weight' => SORT_ASC],
'desc' => ['faq.faq_weight' => SORT_DESC],
'label' => 'Weight'
],
'faqIsFeaturedName' => [
'asc' => ['faq.faq_is_featured' => SORT_ASC],
'desc' => ['faq.faq_is_featured' => SORT_DESC],
356
357
]
]);
if (!($this->load($params) && $this->validate())) {
$query->joinWith(['faqCategory']);
return $dataProvider;
}
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
'id');
'faq_category_id');
'faq_weight');
'faq_is_featured');
'created_by');
'updated_by');
'faq_question', true);
'faq_answer', true);
// filter by category
$query->joinWith(['faqCategory' => function ($q) {
$q->andFilterWhere(['=', 'faq_category.faq_category_name',
$this->faqCategoryName]);
}]);
return $dataProvider;
}
protected function addSearchParameter($query, $attribute, $partialMatch = false)
{
if (($pos = strrpos($attribute, '.')) !== false) {
$modelAttribute = substr($attribute, $pos + 1);
} else {
$modelAttribute = $attribute;
358
}
$value = $this->$modelAttribute;
if (trim($value) === '') {
return;
}
/*
* The following line is additionally added for right aliasing
* of columns so filtering happen correctly in the self join
*/
$attribute = "faq.$attribute";
if ($partialMatch) {
$query->andWhere(['like', $attribute, $value]);
} else {
$query->andWhere([$attribute => $value]);
}
}
}
You may notice that we have use statements on this search model that we have not used before:
use yii\data\ArrayDataProvider;
use yii\db\ActiveQuery;
use yii\db\Query;
We will need those later when we modify this class, so I included them now.
The other thing that is different is that we have specified a default order:
'defaultOrder' => [
'faq_weight' => SORT_ASC,
],
This will set the default order of results to ascending on faq_weight, so to control the order we see
them in, all we have to do is assign the faq_weight according through the UI. Please note that this
method is for backend UI only. As promised, we will be doing it differently for the frontend, and
that will involve using a different method. I will explain more on that when we get there.
Ok, lets make our typical change to behaviors on the controller:
Gist:
Faq Behaviors
From book:
public function behaviors()
{
return [
'access' => [
'class' => \yii\filters\AccessControl::className(),
'only' => ['index', 'view','create', 'update', 'delete'],
'rules' => [
[
'actions' => ['index', 'view', 'create', 'update', 'delete'],
'allow' => true,
'roles' => ['@'],
'matchCallback' => function ($rule, $action) {
return PermissionHelpers::requireMinimumRole('Admin')
&& PermissionHelpers::requireStatus('Active');
}
],
],
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
];
}
359
Faq Form
From book:
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/* @var $this yii\web\View */
/* @var $model backend\models\Faq */
/* @var $form yii\widgets\ActiveForm */
?>
<div class="faq-form">
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'faq_question')
->textInput(['maxlength' => 255]) ?>
<?= $form->field($model, 'faq_answer')->
textArea(['maxlength' => 1055, 'rows' =>10]) ?>
<?= $form->field($model, 'faq_category_id')->
dropDownList($model->faqCategoryList,
[ 'prompt' => 'Please Choose One' ]);?>
<?= $form->field($model, 'faq_is_featured')->
dropDownList($model->faqIsFeaturedList,
[ 'prompt' => 'Please Choose One' ]);?>
<?= $form->field($model, 'faq_weight')->textInput() ?>
<div class="form-group">
<?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update',
['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end(); ?>
360
361
</div>
Something new we did there was change faq_answer to a textArea. And with the textArea method,
you can specify the number of rows, which we have set to 10.
Lets move on to the _search view.
Gist:
Faq Search Partial
From book:
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/* @var $this yii\web\View */
/* @var $model backend\models\search\FaqSearch */
/* @var $form yii\widgets\ActiveForm */
?>
<div class="faq-search">
<?php $form = ActiveForm::begin([
'action' => ['index'],
'method' => 'get',
]); ?>
<?= $form->field($model, 'id') ?>
<?= $form->field($model, 'faq_question') ?>
<?= $form->field($model, 'faq_answer') ?>
<?= $form->field($model, 'faq_category_id')->
dropDownList($model->getFaqCategoryList(),
[ 'prompt' => 'Please Choose One' ]);?>
<?= $form->field($model, 'faq_is_featured')->
dropDownList($model->faqIsFeaturedList,
[ 'prompt' => 'Please Choose One' ]);?>
<?= $form->field($model, 'faq_weight') ?>
<div class="form-group">
<?= Html::submitButton('Search',
['class' => 'btn btn-primary']) ?>
<?= Html::resetButton('Reset',
['class' => 'btn btn-default']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
362
363
'confirm' => 'Are you sure you want to delete this item?',
'method' => 'post',
],
]) ?>
</p>
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'faq_question',
'faq_answer',
'faqCategory.faq_category_name',
'faq_weight',
['attribute'=>'faq_is_featured', 'format'=>'boolean'],
['attribute'=>'createdByUsername', 'format'=>'raw'],
['attribute'=>'updatedByUsername', 'format'=>'raw'],
'created_at',
'updated_at',
],
]) ?>
</div>
You can see we changed the title and made the attribute changes to DetailView widget so we can
get the proper format when we view the record.
Now lets work on the index.php file for Faq.
Gist:
Faq Index
From book:
<?php
use yii\helpers\Html;
use yii\grid\GridView;
use \yii\bootstrap\Collapse;
/* @var $this yii\web\View */
/* @var $searchModel backend\models\search\FaqSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */
$this->title = 'Faqs';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="faq-index">
<h1><?= Html::encode($this->title) ?></h1>
<?php
echo Collapse::widget([
'items' => [
// equivalent to the above
[
'label' => 'Search',
'content' => $this->render('_search', ['model' => $searchModel]) ,
// open its content by default
//'contentOptions' => ['class' => 'in']
],
]
]); ?>
<p>
<?= Html::a('Create Faq', ['create'],
['class' => 'btn btn-success']) ?>
</p>
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'id',
'faq_question',
'faq_answer',
['attribute'=>'faqCategoryName', 'format'=>'raw'],
'faq_weight',
['attribute'=>'faqIsFeaturedName', 'format'=>'raw'],
364
365
</div>
That should look familiar. One difference however is that we used our getFaqIsFeaturedName
method (via magic call) to display the yes or no for boolean. We could have used:
['attribute'=>'faq_is_featured', 'format'=>'boolean'],
But I thought it was best to use the magic call because we are using that method in our search
method and its best to be consistent, since our search model returns the results on index.
If you havent already done so, create at least six Faq records, so you can see how all this looks.
So now were going to move on to creating the frontend version of the Faqs. Although we could use
the same dataprovider and widgets that we are using in the backend, I want to free us from that and
at the same time, demonstrate the use of the ArrayDataProvider class, which returns results in an
array, where you can easily set things like the order and which attributes to return.
Well start by adding a method to our FaqSearch model named frontendProvider. Im just calling it
that so I can easily determine its use from the name.
Like I said, the purpose of this method is to provide the data for a list of FAQs sorted by faq_weight.
This will allow the end user to control where the question appears on the list, simply by adjusting
the weight up or down.
So go ahead and add this method to FaqSearch.php.
Gist:
Frontend Provider
From book:
public function frontendProvider()
{
$query = new Query;
$provider = new ArrayDataProvider([
'allModels' => $query->from('faq')->all(),
'sort' => [
'defaultOrder' => [
'faq_weight' => SORT_ASC,
],
'attributes' => ['faq_question', 'faq_answer',
'faq_weight'],
],
366
'pagination' => [
'pageSize' => 10,
],
]);
return $provider;
}
Ok, lets step through it. We start by creating an instance of Query, which will allow us to create a
query within the instance of ArrayDataProvider. Please note that we need to pull in the following
for this to work:
use yii\data\ArrayDataProvider;
use yii\db\ActiveQuery;
use yii\db\Query;
So make sure the use statements are in the appropriate place at the top of the file. Yii 2 does an
excellent job of complaining, so if you do leave one out, it will let you know whats missing.
Ok, back to the code. I referenced for this example:
http://www.yiiframework.com/doc-2.0/guide-output-data-providers.html
So if you check the guide you will see it is exactly in that format.
The ArrayDataProvider is configured with the type of array config we see in many places on Yii 2.
In this case, we have an allModels key, which points to a query as value, which returns all the faq
record results. And since we are using ArrrayDataProvider, we know we will be returning an array.
The guide doesnt show you how to set a default order, but I figured that out. We set the default
order to faq_value, SORT_ASC, which means it will sort in ascending value. This means an FAQ
with a value of 1 will come 1st on the list.
Then we tell it what attributes we want in the array. In this case, since we are not using this for
backend record creation and maintenance, we can include fewer attributes. In this case, we only
need:
'attributes' => ['faq_question', 'faq_answer', 'faq_weight'],
We are also setting page size for pagination to 10. You probably wont need that, but if you do, its
set. If you have more than 10 records, you also need to setup pagination, but we will do that later
for another iteration, its not necessary now. So this is pretty simple stuff.
Next we need to create a frontend controller for Faq, that uses the backend Faq model and backend
FaqSearch model. Were only going to have 2 actions view and index. There is no need for create,
367
update or delete actions because they are handled in the backend by admin, not in the frontend by
users. Because these are FAQs, we want them visible to all users, so we wont be restricting access,
so this makes the controller fairly simple.
Here is the entire controller.
Gist:
Faq Frontend Controller
From book:
<?php
namespace frontend\controllers;
use
use
use
use
use
use
Yii;
backend\models\Faq;
backend\models\search\FaqSearch;
yii\web\Controller;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
/**
* FaqController implements the CRUD actions for Faq model.
*/
class FaqController extends Controller
{
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
];
}
/**
* Lists all Faq models.
* @return mixed
*/
368
}
/**
* Displays a single Faq model.
* @param integer $id
* @return mixed
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}
protected function findModel($id)
{
if (($model = Faq::findOne($id)) !== null) {
return $model;
} else {
throw new NotFoundHttpException
('The requested page does not exist.');
}
}
}
Its really very simple. On the index method, we call a new instance of FaqSearch model, which
allows us to set an instance of $searchModel->frontendProvider() to $provider, which we can pass
369
to the view. So now the view will have access to the ArrayDataProvider that we made, with the sort
by ascending value on faq_weight.
In the view, to display this, we need the following in frontend/views/faq/index.php. Dont forget to
create the faq folder in frontend/views.
Gist:
Frontend Faq Index
From book:
<?php
use yii\helpers\Html;
use yii\helpers\ArrayHelper;
use yii\helpers\Url;
$this->title = 'FAQs';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="site-about">
<h1><?= Html::encode($this->title) ?></h1>
</BR>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
Questions
</h3>
</div>
<?php
$data = $provider->getModels();
$questions = Arrayhelper::map($data, 'faq_question', 'id');
foreach ($questions as $question => $id){
$url = Url::to(['faq/view', 'id'=>$id]);
$options = [];
echo '<div class="panel-body">'.
Html::a($question, $url, $options) .'</div>';
}
370
?>
</div>
</div>
So now were setting $provider->getModels() to $data. If you look at the guide, this is the method
they use in their example, so that is exactly what I used. Then I created key value pairs out of
question and Id by using the ArrayHelper::map method. If you try this with faq records that have
different faq_weight settings, you will see that it respects the ordering by faq_weight. So you can
absolutely hand control over the order to your client by providing backend UI that lets them set the
faq_weight.
Next Im using a foreach loop to do something with each question and its corresponding id. In this
case, the something is first creating a url that has $id as a parameter. Then Im echoing out each line,
using the Html::a method to create a link that uses $question as the link text and $url as the url to
the view.
I threw in a little bootstrap styling on this page for fun. You can play with raw bootstrap at:
Layoutit.com
They feature drag and drop design, then you can copy your code, so its perfect for designing little
snippets like the above. The resulting index page should look like this:
Getting back to the faq list, the links we created in our foreach loop will send a request to the view
action on the frontend faq controller:
371
It takes in the $id from the get variable, which was set in the url, and calls an instance of the view,
using the correct instance of the model by using the last method on the controller:
protected function findModel($id)
{
if (($model = Faq::findOne($id)) !== null) {
return $model;
} else {
throw new NotFoundHttpException
('The requested page does not exist.');
}
}
You can see how clean and simple all that is.
So for this to work, in frontend/views/faq/view.php, we need the following.
Gist:
Frontend Faq View
From book:
<?php
use yii\helpers\Html;
$this->title = 'FAQ: '. $model->faq_question;
$this->params['breadcrumbs'][] = ['label' => 'FAQ', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
</br>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
372
<h1>
</h3>
</div>
<?= '<div class="panel-body"><h3>'.
$model->faq_answer .'</h3></div>';?>
</div>
The relevant parts here are $model->faq_question and $model->faq_answer, which are made
available to us because we used findModel to find that instance of the Faq model and sent it to
the view from the controller.
One last task, lets just pop it into our top nav in frontend/views/layouts/main.php. Add the new
link to the first $menuItems array:
$menuItems = [
['label'
['label'
['label'
['label'
=>
=>
=>
=>
];
Thats a one line change, so no Gist for that. A question came up from a reader about whether or
not to include the / before the controller. In the example that Yii 2 shows in the Guide, there is no
forward slash. The code that comes out of the box with the template, includes the forward slash.
Both ways work. So its up to you which convention you want to follow.
And thats pretty much it. Im sure you can make it prettier, since Im not much of a UI designer,
but you get the main point here, which is how to move the FAQ data around and keep control over
the order, which your clients will love.
I purposely avoided using the Listview and GridView widgets in this example to show you how you
can use things like ArrayDataProvider without them. You can customize your UI as you wish, you
are not limited to the widgets.
Hopefully when you have a meeting with your client and you suggest to them how you can give
them control over the order of their FAQs, they will be impressed, which is the desired result. We
want to please those really difficult clients because that is how we get paid.
Although we didnt use FaqCategory on the presentation side, we could easily do so by making
similar modfications to the FaqCategorySearch model and implementing from there. Thats a benefit
of putting some forethought into the data structure. So once again, if the client asks for that, you are
one step ahead.
373
Test Controller
Sometimes in development, you want to isolate a block of code and play with it, without cluttering
up your other code. Now version control should always be there if you made a huge mistake, but
you really dont want to be relying on that to step backwards unless you absolutely have to.
So what well do is create a controller named test. We start by navigating to Gii, and clicking on
Controller Generator. We dont have to worry about creating a model first, since we dont have one,
our test controller will be used to play around with different bits of many models, and we will pull
in the models via use statements as we need them.
From our controller generator on Gii, pop in test into the first input field to name the controller. We
will create just one action for now, Index. Then check to see if the namespace is set to where you
want it to go.
Ive created a test controller for both frontend and backend. Thats not really necessary, but it is
convenient. It just helps me when Im thinking of things to intuitively associate a backend test
controller with things I want to try in the backend. Also, sometimes, I leave code in place in the test
controller while Im thinking about whether or not I want to use it.
Ok, so when you hit preview on Gii, it will show two files, the controller and the view file. Once we
click generate, it will also create the directory to hold the view file. So lets do that now.
If all went well, you should have a TestController.php file in frontend/controllers and a frontend/views/test/index.php file.
Ok, so lets look at the TestController.php.
Gist:
Test controller
From book:
<?php
namespace frontend\controllers;
class TestController extends \yii\web\Controller
{
public function actionIndex()
{
return $this->render('index');
}
}
374
Thats it. You can see it consists of very little except the actionIndex method, and all that is doing is
rendering a view named index. Lets look at the view.
Gist:
Test Index View
From book:
<?php
/* @var $this yii\web\View */
?>
<h1>test/index</h1>
<p>
You may change the content of this page by modifying
the file <code><?= __FILE__; ?></code>.
</p>
Again, almost nothing here, so very easy to play with. Try yii2build.com/index.php?r=test and you
should see the view.
Ok, so think of it as a blank canvass that you can use to try different snippets of code or to figure
out how something works or what is contained in var_dump().
You should probably create a test controller for both frontend and backend. I have a feeling this is
going to come in very handy, very soon.
Note, you may have to specify the view path if it doesnt generate the view file, which is what
happened when I did it:
/var/www/yii2build/backend/views/test
Please keep in mind your path may differ if you path to the site folder is different. If you get your
files in an unwanted location, either delete and try again or move the existing files. Just make sure
you get the namespaces correct.
Components
Were going to setup a components folder for the purpose of holding our custom widget, which we
have not yet made. In the Yii 2 guide, they use an example where they show the widget residing in
a component folder, so we will follow as close to that as possible.
So we start by creating a new folder in our project directory named components. Your directory tree
should look like this:
375
We will be testing our setup by creating a component, which is different than a widget, but will
reside in the same folder. Widgets are used in views and display something to the user. Components
typically work behind the scenes. You will get a better idea of this by example.
So lets create MyComponent.php inside the components folder.
Gist:
MyComponent.php
From book:
<?php
namespace components;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
class MyComponent extends Component
{
public function blastOff()
{
echo "Houston, we have ignition...";
}
}
To create a component, we extend off of Component. Dont forget to include the necessary use
statements.
Then we simply create the method we wish to call, in our case we are creating a blastOff method.
Im just using a simple echo statement for demonstration purposes.
376
Blastoff, for our international readers who might not understand that phrase in English, is what
happens when a rocket launches. Houston we have ignition is what the ground controllers say to
mission control when the rocket engine ignites. Its turned into a joke in English meaning things are
underway, something has started, etc., usually something dramatic.
Anyway, back to the component. Now if you were to try to use this component from inside one of
your test controllers, it would return an error because the autoloader cant see the file yet. We have
to modify our common/config/bootstrap.php file to give our application visibility on the folder.
Change it to the following:
Gist:
Common/Config/Bootstrap
From book:
<?php
Yii::setAlias('common', dirname(__DIR__));
Yii::setAlias('frontend', dirname(dirname(__DIR__)) . '/frontend');
Yii::setAlias('backend', dirname(dirname(__DIR__)) . '/backend');
Yii::setAlias('components', dirname(dirname(__DIR__)) . '/components');
Yii::setAlias('console', dirname(dirname(__DIR__)) . '/console');
This is using the setAlias method to create an alias to our folder. You can read about the setAlias
method in the guide, they explain it better than I can:
Yii 2 setAlias from Guide
Now we need just one more step, we have to modify our components array in common/config/main.php to reference MyComponent.php. Im going to give you the whole file because one little
thing out of place and nothing works.
Please note, you will have enter your facebook app id and secret again because Im using generic
placeholders for those values.
Gist:
Config/Main
From book:
<?php
return [
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'extensions' => require(__DIR__ . '/../../vendor/yiisoft/extensions.php'),
'modules' => [
'social' => [
// the module class
'class' => 'kartik\social\Module',
// the global settings for the disqus widget
'disqus' => [
'settings' => ['shortname' => 'DISQUS_SHORTNAME'] // default settings
],
// the global settings for the facebook plugins widget
'facebook' => [
'appId' => 'your id',
'secret' => 'your secret',
],
// the global settings for the google plugins widget
'google' => [
'clientId' => 'GOOGLE_API_CLIENT_ID',
'pageId' => 'GOOGLE_PLUS_PAGE_ID',
'profileId' => 'GOOGLE_PLUS_PROFILE_ID',
],
// the global settings for the google analytic plugin widget
'googleAnalytics' => [
'id' => 'TRACKING_ID',
'domain' => 'TRACKING_DOMAIN',
],
// the global settings for the twitter plugin widget
'twitter' => [
'screenName' => 'TWITTER_SCREEN_NAME'
],
],
// your other modules
],
'components' => [
377
378
'cache' => [
'class' => 'yii\caching\FileCache',
],
'mycomponent' => [
'class' => 'components\MyComponent',
],
],
];
Next we need to make sure we can blastOff as planned, so modify the index action on a test controller.
Use frontend to follow my example exactly, to:
Gist:
Test Controller blastOff
From book:
<?php
namespace frontend\controllers;
use yii;
class TestController extends \yii\web\Controller
{
public function actionIndex()
{
Yii::$app->mycomponent->blastOff();
}
}
Dont forget the use statement. To test this simply point your browser to:
http://yii2build.com/index.php?r=test
If all went well, you were able to blastOff. And now you have a working components folder.
379
return $featuredProvider;
}
Now we need a way to test this, so lets use our backend test controller.
Modify backend/controllers/TestController.php.
Gist:
Backend Test Controller
From book:
<?php
namespace backend\controllers;
use
use
use
use
use
use
Yii;
backend\models\Faq;
backend\models\search\FaqSearch;
yii\web\Controller;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
380
381
{
$searchModel = new FaqSearch();
$provider = $searchModel->featuredProvider();
return $this->render('index', [
'searchModel' => $searchModel,
'provider' => $provider,
]);
}
}
Really the only change here is which method we call from $searchModel, in this case featuredProvider. Were not doing anything with behaviors because that is not what we are testing for.
One more step to test our method, and that is to setup backend/views/test/index.php.
Gist:
Backend Test Index
From book:
<?php
use yii\helpers\Html;
use yii\helpers\ArrayHelper;
use yii\helpers\Url;
$this->title = 'FAQs';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="site-about">
<h1><?= Html::encode($this->title) ?></h1>
</BR>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
Questions
382
</h3>
</div>
<?php
$data = $provider->getModels();
$questions = Arrayhelper::map($data, 'faq_question', 'id');
foreach ($questions as $question => $id){
$url = Url::to(['faq/view', 'id'=>$id]);
$options = [];
echo '<div class="panel-body">'.
Html::a($question, $url, $options) .'</div>';
}
?>
</div>
</div>
Now that should look familiar, all we did was copy the frontend/views/faq/index.php file into
backend/views/test/index.php.
So just make sure you have 5 featured FAQs created, with different weight for each, and a couple
of FAQs that are not featured, so you can verify your results. Pay careful attention to the order to
make sure its right. It should work as planned.
I think this is a great opportunity to explore some alternatives on how we can do this. First, lets
try using SqlDataProvider, which lets us use a sql statement. For those of us more used to working
with SQL, thats actually easier to work with.
Gist:
FaqSearch SqlDataProvider
From book:
383
Here we are using SQL to tell the query what order to return the results in.
We start with a count query to use for pagination. Then we have the main query, where we just
explicitly tell it in SQL that we want all results from faq where faq_is_featured = 1 and to order by
faq_weight ascending.
This works perfectly, but because we are not using one of Yii 2s widgets, the pagination does not
work.
This exposes a flaw in our approach because we might have enough results that would warrant use
of pagination, and its a shame not to take advantage of Yii 2s pagination object and LinkPager
widget because they are very useful.
384
So looking over the docs, I found a more concise way to do all this and still get everything we want.
We are going to ignore the search model method and go straight to our controller.
Gist:
Backend Test Controller FAQ
From book:
<?php
namespace backend\controllers;
use
use
use
use
use
use
use
Yii;
backend\models\Faq;
backend\models\search\FaqSearch;
yii\web\Controller;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
yii\data\Pagination;
385
Now that is really leveraging the power of Yii 2. Lets go through it carefully.
$query = Faq::find()->where(['faq_is_featured' => 1]);
We start by setting up our query, super easy syntax to understand at this point. Next we tell it how
we want to order the query:
$query->orderBy(['faq_weight' => SORT_ASC]);
Yii 2s syntax is so simple, we dont need to explain it. Next we make a copy of the query object so
we can use it to return a count, which we will feed into our pagination object.
$countQuery = clone $query;
Notice the use of clone, super efficient and easy to use. We used clone because we need a separate
copy of our query to return a count.
Next we create the pagination object:
$pages = new Pagination(['defaultPageSize' => 3,
'totalCount' => $countQuery->count()]);
Notice we are handing the config for the pagination right in through the constructor. I set the page
size to 3 , so I could easily test it. And of course we use $countQuery to return the number of results,
so our pagination can do its calculations.
Now we can set up the object $models:
386
$models = $query->offset($pages->offset)
->limit($pages->limit)
->all();
You can see we have handed in the $pages object into the offset method of query, which is how it
sets the record limit for the page.
Then comes the render method:
return $this->render('index', [
'models' => $models,
'pages' => $pages,
]);
And we are passing along our two objects, $models and $pages into the view.
Now we need to set up the Index view.
Gist:
Test Index FAQ
From book:
<?php
use yii\helpers\Html;
use yii\helpers\Url;
use yii\widgets\LinkPager;
$this->title = 'FAQs';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="site-about">
<h1><?= Html::encode($this->title) ?></h1>
</BR>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
Questions
</h3>
</div>
387
<?php
foreach ($models as $model){
$url = Url::to(['faq/view', 'id'=>$model->id]);
$options = [];
echo '<div class="panel-body">'.
Html::a($model->faq_question, $url, $options) .'</div>';
}
echo LinkPager::widget([
'pagination' => $pages,
]);
?>
</div>
</div>
So you can see how much more concise this is. No need to setup an array, since we are working with
the object. $models goes into our foreach loop and $pages goes into our LinkPager widget. And just
like that, you have pagination that works.
Now if you click on one of the Faq links, youll see it goes to a backend view page. Thats not what
we want, but its ok for now. Once we have created our widget, we will call it from within the
frontend and the links will go to the frontend view pages.
Its clear that we got a lot of use out of our test controller. It allowed us to flush out what we were
looking for in the logic of our widget. Now we know exactly how we are going to format the query.
Ok, so now were ready to tackle the widget directly. Lets start by creating a folder inside of
components named views. This will hold the view for our widget.
Then lets create a blank file for the widget named FaqWidget.php and put that in the components
folder. Make sure to follow these instructions carefully, nothing will work otherwise.
So your directory tree should look like the following:
388
Directory Tree
Now we need to take the next step, which is to include FaqWidget in our common/config/main.php
file. Im only going to show the components array. You should have the complete file from the
components section, so we will simply be adding to it here.
Gist:
Components Config
From book:
'components' => [
'cache' => [
'class' => 'yii\caching\FileCache',
],
'mycomponent' => [
'class' => 'components\MyComponent',
],
'faqwidget' => [
'class' => 'components\FaqWidget',
],
],
You can see we are supplying it with the classname lowercase, then the namespace, which identifies
where the class resides. This allows the autoloader to map it correctly so we can use the class via
use statement.
Before we can test anything, we need to create the FaqWidget class and the corresponding view.
Lets start with FaqWidget.php.
Gist:
FaqWidget.php
From book:
<?php
namespace components;
use
use
use
use
use
use
use
use
yii\base\Widget;
yii\helpers\Html;
Yii;
backend\models\Faq;
backend\models\search\FaqSearch;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
yii\data\Pagination;
389
390
yii\base\Widget;
yii\helpers\Html;
Yii;
backend\models\Faq;
backend\models\search\FaqSearch;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
yii\data\Pagination;
Obviously we are using components as the namespace and pulling in everything we need via use
statements.
Next we declare the class and two public properties:
class FaqWidget extends Widget
{
public $models;
public $pages;
391
We start the method by calling parent::init. So what you need to know about init is that it acts
like a constructor, its going to run when the class is called. That means all the necessary logic
will be performed and you can see the logic is simply what we built in our test controller, with
one difference. We had to assign class properties $pages and $model using $this->models and $this>pages. That way we can access the values from our other method, run():
public function run()
{
return $this->render('faq', [
'models' => $this->models,
'pages' => $this->pages,
]);
}
In this case, run is returning a render method to call our widget view faq. So just to recap, the init
does the query, assigns the values to our class properties, which are then passed into our run method
via $this.
I found this to be a very intuitive flow. Its like a mini-controller calling a view.
So now lets get our view going. Inside of the component/views folder, create the following file,
faq.php.
Gist:
Faq View
From book:
<?php
use yii\helpers\Html;
use yii\helpers\Url;
use yii\widgets\LinkPager;
?>
<div class="site-about">
</BR>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
Featured Questions
</h3>
</div>
392
<?php
foreach ($models as $model){
$url = Url::to(['faq/view', 'id'=>$model->id]);
$options = [];
echo '<div class="panel-body">'.
Html::a($model->faq_question, $url, $options) .'</div>';
}
echo LinkPager::widget([
'pagination' => $pages,
]);
?>
</div>
We just copied from our Test index view page and chopped out the stuff we didnt need like the title
and the h1.
Ok, so were ready to make this work. Lets go to frontend/views/site/index.php and put this between
the last and 2nd to last div tags like so:
</div>
<?= FaqWidget::widget() ?>
</div>
No need to supply a gist for a single short line. How simple is that? Oh, but dont forget the use
statement at the top of the file:
use components\FaqWidget;
And now if you save and refresh your index page, you should see the following at the bottom of the
page:
393
So now you have a paginated widget for featured faq that you can embed anywhere on the site,
which also respects the order according to weight, and only includes featured questions. Its also
styled in Bootstrap, which means it resizes with device size. How cool is that?
Its pretty easy to add an optional parameter to the widget, so Im going to cover that. Lets make
the pagination page size a parameter that we can hand in.
We start in our FaqWidget class by declaring a new property:
public $pageSize;
This will hold the value of the page size when we hand it in or get set by our if statement, which we
will add now:
parent::init();
if ($this->pageSize === null) {
$this->pageSize = 2;
}
394
If you set it to 5 and you only have 5 featured records, LinkPager will not show you the pagination
count, so this is a very smart widget to play with.
Here is the entire FaqWidget.php file for reference.
Gist:
FaqWidget
From book:
<?php
namespace components;
use
use
use
use
use
use
use
use
yii\base\Widget;
yii\helpers\Html;
Yii;
backend\models\Faq;
backend\models\search\FaqSearch;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
yii\data\Pagination;
395
}
public function run()
{
return $this->render('faq', [
'models' => $this->models,
'pages' => $this->pages,
]);
}
}
So I was really happy with that, but then I thought about our difficult-to-please client again. If they
like the Featured Faq widget a lot, they might want a version of it for all FAQs. In fact they might
change their mind several times about that. So what should we do? How can we make this easy on
ourselves?
Well, instead of taking in a single parameter, why dont we take in an array of settings? Then we
could test for something like featuredOnly set to true or false, and depending on the answer, give
them either just the featured FAQs or all of them.
If we look at it from the widget call and work our way backwards, it will be easy to see how this
will work:
<?= FaqWidget::widget(['settings' => ['pageSize' => 3,
'featuredOnly' => true]]) ?>
So now were handing in an array named settings, with two key value pairs. As long as we have an
class property to hold the value of settings we are good to go. We can then access it via keyword
$this to test the values and perform the appropriate logic based on the results.
Here is the updated FaqWidget.php file.
Gist:
FaqWidget With Settings
From book:
<?php
namespace components;
use
use
use
use
use
use
use
use
yii\base\Widget;
yii\helpers\Html;
Yii;
backend\models\Faq;
backend\models\search\FaqSearch;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
yii\data\Pagination;
396
397
}
public function run()
{
return $this->render('faq', [
'models' => $this->models,
'pages' => $this->pages,
'settings' => $this->settings,
]);
}
}
Ok, lets step through this. You can see we now have a settings array that is initialized to an empty
array.
class FaqWidget extends Widget
{
public $models;
public $pages;
public $settings = [];
The widget method of the Widget class will take in the values that we pass into the signature of the
widget method and assign them to this array because it looks for a property with the same name.
This is clever stuff and very powerful. It opens up a whole range of possibilities for you when you
are making widgets.
In our case, were only handing in two key value pairs, so this is still a relatively simple
implementation. You can see how we then use the values in the array to test against.
if (!isset($this->settings['pageSize'])) {
$this->settings['pageSize'] = 2;
}
398
So now we test against the array keys to perform our logic on the values. In the case of the second
if statement, we are looking for a key featuredOnly and if its value is set to true, then query only
where featured is equal to one, otherwise get all records.
Then we just modify our render method accordingly:
public function run()
{
return $this->render('faq', [
'models' => $this->models,
'pages' => $this->pages,
'settings' => $this->settings,
]);
}
Youll notice we are sending the $settings array into the view. The reason for this is that we want to
test against the featuredOnly key, and depending on the results, show a different heading.
Gist:
Faq View
From book:
<?php
use yii\helpers\Html;
use yii\helpers\Url;
use yii\widgets\LinkPager;
?>
<div class="site-about">
</BR>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<?php
if ($settings['featuredOnly'] == true){
echo 'Featured Questions';
} else {
echo 'FAQs';
}
?>
399
</h3>
</div>
<?php
foreach ($models as $model){
$url = Url::to(['faq/view', 'id'=>$model->id]);
$options = [];
echo '<div class="panel-body">'.
Html::a($model->faq_question, $url, $options) .'</div>';
}
echo LinkPager::widget([
'pagination' => $pages,
]);
?>
</div>
</div>
We test to see if we are only using featuredOnly, and if so, show the appropriate heading.
Alternatively, you could use ternary syntax and that would be fine too.
Then to wrap this up, we just make the widget call, which, even though it got a little bit longer, is
still a single line of code, if you are not avoiding word wrap of course:
<?= FaqWidget::widget(['settings' =>
['pageSize' => 3,'featuredOnly' => true]]) ?>
And thats it. You now have a configurable FAQ widget that you can use in any view, with a single
line call. Just remember to include your use statements if you do place it elsewhere on the site.
CDN
Ok, so many of you may already know that using a CDN, a content delivery network, to pull in css,
js, and jquery assets can dramatically speed up performance of your site.
400
The reason for this is that the CDN versions are typically already cached in the users browser, so
they dont have to pull down the library every time they visit a site. And because these assets are so
common, chances are your site will not be the first they visit that utilizes these assets, so most likely
they are already cached. This can make a huge difference in the speed of the page loading.
Theres a really simple way to do this in common/config/main.php. For convenience, Im going to
give you the entire file, but dont forget I have generic placeholders for Facebook app id and secret.
Gist:
Main with CDN
From book:
<?php
return [
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'extensions' => require(__DIR__ . '/../../vendor/yiisoft/extensions.php'),
'modules' => [
'social' => [
// the module class
'class' => 'kartik\social\Module',
// the global settings for the disqus widget
'disqus' => [
'settings' => ['shortname' => 'DISQUS_SHORTNAME']
],
// the global settings for the facebook plugins widget
'facebook' => [
'appId' => 'your app id',
'secret' => 'your secret',
],
// the global settings for the google plugins widget
'google' => [
'clientId' => 'GOOGLE_API_CLIENT_ID',
'pageId' => 'GOOGLE_PLUS_PAGE_ID',
'profileId' => 'GOOGLE_PLUS_PROFILE_ID',
],
// the global settings for the google analytic plugin widget
'googleAnalytics' => [
'id' => 'TRACKING_ID',
'domain' => 'TRACKING_DOMAIN',
],
// the global settings for the twitter plugin widget
'twitter' => [
'screenName' => 'TWITTER_SCREEN_NAME'
],
],
// your other modules
],
'components' => [
'assetManager' => [
'bundles' => [
// use bootstrap css from CDN
'yii\bootstrap\BootstrapAsset' => [
'sourcePath' => null,
// do not use file from our server
'css' => [
'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css']
],
// use fontawesome css from CDN
'frontend\assets\FontAwesomeAsset' => [
'sourcePath' => null,
// do not use file from our server
'css' => [
'https://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css']
],
// use fontawesome css from CDN
'backend\assets\FontAwesomeAsset' => [
'sourcePath' => null,
// do not use file from our server
'css' => [
'https://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css']
],
// use bootstrap js from CDN
'yii\bootstrap\BootstrapPluginAsset' => [
'sourcePath' => null,
// do not use file from our server
'js' =>
'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/js/bootstrap.min.js']
],
// use jquery from CDN
'yii\web\JqueryAsset' => [
'sourcePath' => null,
// do not publish the bundle
'js' => [
401
402
'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js',
]
],
],
],
'cache' => [
'class' => 'yii\caching\FileCache',
],
'mycomponent' => [
'class' => 'components\MyComponent',
],
'faqwidget' => [
'class' => 'components\FaqWidget',
],
],
];
By setting the source path to null, we are unsetting the path, then setting it to the CDN. The only
other notable thing here is that we had to do font-awesome for both frontend and backend, since
we created assets for font-awesome in both places.
Once these changes are in place, I think youll find your application moves a lot quicker, which is
always a plus.
Summary
Congratulations, youve completed the first bonus chapter. Weve added some cool features to the
template, and in the process of doing so, have learned much more about Yii 2.
Thats about as much of a conclusion as I can draw right now because this isnt really the end of
the book. I plan on adding new material on a go forward basis. Yii 2 is the primary PHP framework
that I use personally, and I think so highly of it, I want to continue to share it with you.
Ive come to understand that writing about a framework is more of a journey than a destination.
For example, Yii 2 will be doing periodic version releases and things can change.
Ill do my best to stay on top of the changes and revise the book accordingly. For basic function
updates from Yii 2, adjustments will be made to the core book.
The bonus chapters, on the other hand, will simply be added to successively, unless a method has to
change due to a versioning change in Yii 2 itself. Successive additions gives us the sense that we are
building on something.
403
Your purchase of Yii 2 For Beginners entitles you to free updates for the life of the book, so there is
no reason not to benefit from that. Just look for update notices by email or check the leanpub.com
website for the last update.
Now Ill take a moment to thank all the readers that wrote to me with positive comments and typo
notifications. It really helps me in my pursuit to take Yii 2 For Beginners to the highest level of
quality for a technical book. With your help, we can do it.
When I get reader feedback, it encourages me to push forward. As always, comments, links, reviews
and word-of-mouth recommendations are greatly appreciated. Lets share this amazing framework
with as many people as we can.
So thanks again everyone, see you soon.
You can see that we have two improvements. First we got rid of index.php, no reason to show that.
Second, we got rid of the ?r= and replaced it with a search engine friendly alternative.
In this chapter, we are also going to implement slugs. For those who dont know, a slug is a string
that describes the content of the page and is embedded in the url. We will be using slugs with our
Faq model, and the url will look something like:
http://yii2start.com/faq/11/should-i-use-a-framework
You can see that not only do we not need view, but we also have the slug added directly to the url
in a nice search engine friendly format. This format increases visibility to the search engine and this
is important if you or the client wishes to have the page found by google and other search engines.
Ive never met anyone who didnt think that was important.
Anyway, all of this is fairly easy to set up, but does require some work.
Pretty URLs
Lets start with the pretty URLs. The steps we need to take are:
Create an htaccess file for both frontend and backend in the web folders.
Modify our Vhost file for apache.
Restart Apache
Modify frontend/config/main.php and backend/config/main.php for urlManager
405
Apache Vhost
From a windows machine we will edit this from notepad, running in administrator mode. Find
notepad from start button on taskbar. Right click and select run in administrator mode.
Notepad will open. Select file open and the path to vhosts, in my case:
C:\xampp\apache\conf\extra\
Then select:
httpd-vhosts.conf
406
Gist:
vHost Entry Apache
From book:
NameVirtualHost *
RewriteEngine on
<VirtualHost yii2build.com>
DocumentRoot "C:\var\www\yii2build\frontend\web"
ServerName localhost
ServerAlias www.yii2build.com
</VirtualHost>
NameVirtualHost *
RewriteEngine on
<VirtualHost yii2build.com>
DocumentRoot "C:\var\www\yii2build\backend\web"
ServerName localhost
ServerAlias backend.yii2build.com
</VirtualHost>
You can see all we did was add the instruction for RewriteEngine on. Go ahead and save and close.
Restart Apache
Make sure to restart apache so the changes take effect.
.htaccess
In the frontend/web, create a file named .htaccess. Dont forget to put the dot in front of the name.
Put the following in the file.
Gist:
.htaccess contents
From book:
407
For reference, Im going to include a Gist to the entire file, in case you need to troubleshoot:
frontend/config/main.php
You can see that we set showScriptName to false, and this gets rid of index.php. The enablePrettyUrl
gets rid of the ?r= syntax, so that is how the two combine to give us the urls we want.
You can also see that as part of the urlManager array, we have a rules section. The rules use regular
expressions, such as w+ and d+ to represent words and numbers. So for example, when we pass the
url a controller/action with id parameter, we account for that like so:
'<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>',
One thing to take note of is that when controllers have multiple words, such as UserType, the
convention in url is user-type, so we have to account for that like so:
408
Remember that you have to configure backend/config/main.php the exact same way for this to work
properly on the entire application. The easy way to do it is just copy the file.
Note, once implemented pretty URLs are implemented, the path to gii is now yii2build.com/gii.
So now that we got our pretty URLs enabled, well move on to creating slugs.
Slugs
We already mentioned what slugs are in the beginning of the chapter. Slugs are a common feature
found on many websites, stackoverflow and yahoo news for example. They add context within the
url and the search engines value this, but there is debate about how much value it adds.
One comment I read on stackoverflow stated that while they didnt see any increase in traffic from
adding slugs, they did see a 300% click through increase.
My own view is that slugs are valuable and worth the time to implement. In certain scenarios, we
can even automate this process.
Sluggable Behavior
Yii 2 has a ready-made behavior that we can use to create slugs on models. Our Faq model provides us
a perfect example of how we can use slugs. There are a number of steps involved in implementation:
This sounds complicated, but once we work through it, youll see how easy it is. Lets start by adding
sluggable behavior to the Faq model:
Gist:
Sluggable Behavior
From book:
409
So you can see in the sluggable behavior, we defined an attribute, faq_question. This is the attribute
the behavior will use to create the slug. You can see we dont have to declare much. We just feed it
the attribute and the behavior does the rest.
Slug Column
Of course for this to work, we need to add a column to the faq table:
410
slug column
You can see the slug column is a varchar(255), not null. Note that although the id column is not
shown, it is there.
Once thats in place, you can create an faq to see if it works.
411
Slugs In Table
Hopefully the image is clear enough for you to see that it automatically dropped the ? from the
faq_question records and put a dash between the words. How easy is that?
While we are successfully creating the slugs, we are not yet seeing them in the url.
Just pop that under the last existing rule and save.
Now we have given the urlManager a way to handle the slug in the url. We still need to modify the
controller and the urls pointing at the view records.
412
Ok, so what we have different here is that we are taking in a second parameter and defaulting it to
null. The reason why we are doing that is that is so we dont have to do a lot of error handling if
the slug is not included in the url.
Next we call up the model record based on the id:
$model = $this->findModel($id);
Even though we allow the $slug to be null in the signature, we require it to match to show the actual
page:
if ($slug == $model->slug){
Then we simply pass along the $model and $model->slug to the view.
return $this->render('view', [
'model' => $model,
'slug' => $model->slug
]);
413
414
In both cases, we had to format the redirect url to account for the slug. We used the Url::toRoute
method to format the url, which allows us to specify a string.
We name the controller with / separator, concatenate the model id, add another / separator and then
concatenate the slug. And thats it!
415
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'id',
'faq_question',
'faq_answer',
['attribute'=>'faqCategoryName', 'format'=>'raw'],
'faq_weight',
['attribute'=>'faqIsFeaturedName', 'format'=>'raw'],
[
'class' => 'yii\grid\ActionColumn',
'template' => '{view} {update} {delete}',
'buttons' => [
'view' => function ($url,$model) {
return Html::a(
'<span class="glyphicon glyphicon-eye-open"
aria-hidden="true"></span>',
$url.'/'.$model->slug);
},
],
],
],
]); ?>
So you can see what is different. We are defining the template and the template tokens, {view}, for
example, match up with the buttons as shown above.
In the buttons array, we define the view button only, the others will use default behavior. So we use
an anonymous function to return the link via the Html::a method.
You can see we are concatenating $model->slug onto the $url:
$url.'/'.$model->slug
So now we have the complete solution for the backend, if you try clicking on your faq records from
the the index page, you will get the slug, which will give you the following format in the url:
416
http://backend.yii2start.com/faq/11/should-i-use-a-framework
If you decide you want a different separator for the words in the slug, you can accomplish that by
modifying the behavior like so:
'sluggable' => [
'class' => SluggableBehavior::className(),
//'attribute' => 'faq_question',
// In case of attribute that contains slug has different name
// 'slugAttribute' => 'alias',
'value' => function ($event) {
$question = rtrim($this->faq_question, '?' );
return str_replace(' ', '_', $question);
}
],
We commented out the attribute setting and added a value setting. For the value of the slug, we
trim off the ? with rtrim. Then we do a simple str_replace to use an underscore instead of the
default minus sign. You can use this format if you need to perform some other calculation, perhaps
something more complicated.
Anyway, you can see how easy this is to work with. In the course of this book, we have used all four
of Yii 2s ready-made behaviors and they are incredibly useful and easy to use.
Ok, so to wrap up our implementation of slugs for Faq, we need to change the frontend.
Since we already have the rules copied into the frontend urlManager, we can move right to the view
of the widget, faq.php, located at components/views/faq.php.
Lets make a simple change. In the foreach loop, remove the following line:
$url = Url::to(['faq/view', 'id'=>$model->id]);
We are again using Yii 2s Url::toRoute method, which allows us to pass in the url as a string.
That takes care of our friendly widget. But what about the Faq link in the header nav?
Thats actually not going to be so easy to change, we will have to change the controller as well as
the index page.
417
Hmm. I wonder what could make this easier on us? If only we had a ready-made solution that we
could just pop in. Wait. We do. Why dont we simply use the widget for this task, since it is already
formatted?
Its true that we still have to change the frontend FaqController, but this is what we need to change
the index action to:
public function actionIndex()
{
return $this->render('index');
}
That one doesnt even require a gist. And here is the view action, exactly the same as the backend
version:
public function actionView($id, $slug = null)
{
$model = $this->findModel($id);
if ($slug == $model->slug){
return $this->render('view', [
'model' => $model,
'slug' => $model->slug
]);
} else {
throw new NotFoundHttpException('The requested Faq does not exist.');
}
}
So again, no Gist necessary, you can copy your backend version if you like.
In the index.php view page, lets change it to the following.
Gist:
Faq View Frontend
From book:
418
use yii\helpers\Url;
use components\FaqWidget;
$this->title = 'FAQs';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="site-about">
<h1><?= Html::encode($this->title) ?></h1>
</div>
<BR>
<?= FaqWidget::widget(['settings' =>
['pageSize' => 10,
'featuredOnly' => false
]])?>
So you can see this actually got easier. Widgets are awesome, look at how much work we saved by
not having to bother with another foreach loop.
One slight thing is not quite right. We are repeating the word Faq in the view page and this doesnt
look right. Why dont we modify our widget to take in another parameter for heading, and then we
can just pass it the value we want?
Because the settings array for our widget takes in the key/value pairs that we hand in from the
method signature, we can just add another pair. All we have to do is change the logic on the widget
view file.
So lets chang faq.php,located at components/views/faq.php. like so:
Gist:
faq.php
From book:
<?php
use yii\helpers\Html;
use yii\helpers\Url;
use yii\widgets\LinkPager;
?>
<div class="site-about">
<div class="panel-heading">
<h3 class="panel-title">
<?= $settings['heading'];?>
</h3>
</div>
<?php
419
420
$options = [];
echo LinkPager::widget([
]);
?>
</div>
</div>
Ok, so now we just need to hand in the parameter in the two widget calls. First lets do the one on
site index.php:
421
And that does it. Now we have more control over our output. Just dont forget to hand in a heading
setting or you will get an error in your Faq.php file.
Alternatively, you could wrap it in an if statement to only display if its set:
<h3 class="panel-title">
<?php
if(isset($settings['heading'])){
echo $settings['heading'];
}
?>
</h3>
Summary
By taking control of the URLs and the slugs, we made our application more visible to the search
engines. And since Yii 2 provides sluggable behavior, we can use it to automatically add slugs to our
models.
This makes it easy and convenient to provide slugs for view pages.
422
Thanks again to all the readers who wrote in to help with typos and other suggestions. Also, we are
getting a lot of great reviews on:
GoodReads.com Yii 2 For Beginners Reviews
Please take a moment if you can to share your thoughts, everyone will appreciate it. Lets share this
amazing framework with as many people as we can.
Just to remind readers who may be reading this chapter as part of their initial purchase of the book,
you get free updates of Yii 2 For Beginners for the life of the book. Just login to your leanpub.com
account and grab the latest version.
I have a lot more bonus material planned, so look for the email announcements and take full
advantage of that.
We will have a new bonus chapter on Yii 2 Authclient shortly, so we can integrate Facebook login
and signup.
Thanks again for continuing to share the Yii 2 journey, see you soon.
Yii2 - AuthClient
Fortunately for us, the team at Yii 2 has a ready made extension, Yii2-authclient, which gives us
everything we need to get started. Im going to link to the section in the Yii Guide for reference:
Yii2-AuthClient
Since this is a fairly new entry into the guide, please keep in mind that it is subject to change, so it
might not match our implementation perfectly. I do my best to stay on top of changes, but this can
easily be missed, so its best for you to reference the guide as well as this book.
At any rate, if it is different, you should be able to work it out. Im going on with the assumption
its the same, and that this solution will work perfectly.
Yii2-AuthClient actually supports multiple social networks, including, Facebook, Twitter, Github,
Google and more. For our purposes, we are going to focus on Facebook and Github, but you can use
what you learn here for the other networks as well.
Ok, so what were going to do:
Add the Yii2-authclient extension to our composer.json and run composer update.
Create an auth table.
Create Auth model.
Add the Yii2-authclient config to our components array in common/config/main.php
Create our auth applications on each provider.
Modify our actions method on the site controller.
Add onAuthSuccess method to the site controller.
Add navigation and links to Facebook and Github signup and sync.
Add a social sync feature for existing accounts to sync with social networks.
Refactor for maintainability.
424
Note that you dont need the comma if its the last line. Then entire block at this point should look
like:
"require": {
"php": ">=5.4.0",
"yiisoft/yii2": "*",
"yiisoft/yii2-bootstrap": "*",
"yiisoft/yii2-swiftmailer": "*",
"kartik-v/yii2-social": "dev-master",
"yiisoft/yii2-authclient": "*",
"fortawesome/font-awesome": "4.2.0"
},
Composer Update
Configuration
The next step is to configure the component. Go to common/config/main.php and add the following
to your components array.
Gist:
Auth Collection
From book:
425
'authClientCollection' => [
'class' => 'yii\authclient\Collection',
'clients' => [
'facebook' => [
'class' => 'yii\authclient\clients\Facebook',
'clientId' => 'your client id',
'clientSecret' => 'your client secret',
],
'github' => [
'class' => 'yii\authclient\clients\GitHub',
'clientId' => 'your client id',
'clientSecret' => 'your client secret',
],
'twitter' => [
'class' => 'yii\authclient\clients\Twitter',
'consumerKey' => 'your consumer key',
'consumerSecret' => 'your consumer secret',
],
'google' => [
'class' => 'yii\authclient\clients\GoogleOAuth',
'clientId' => 'your client id',
'clientSecret' => 'your client secret',
],
'linkedin' => [
'class' => 'yii\authclient\clients\LinkedIn',
'clientId' => 'your client id',
'clientSecret' => 'your client secret',
],
],
],
You can see that we added settings for 5 providers. Obviously, replace the placeholders for your
client id and your client secret with your actual settings.
As of this writing, Facebook, Linkedin, Google and Github work beautifully.
Twitter Issue
Twitter does not return the email address of the user, see the following issue:
426
Provider Applications
Now we need to create our provider applications. For reference, Im going to give you a list of
provider urls to create the apps:
Facebook:
https://developers.facebook.com/
GitHub:
https://github.com/settings/applications
Twitter:
https://apps.twitter.com/
Google:
https://console.developers.google.com/project
LinkedIn:
https://www.linkedin.com/secure/developer
Yii2authclient also supports:
Microsoft Live
VKontakte
Yandex
At this point, I wont be implementing those into the template.
Facebook App
Since we have a Facebook app already, lets start with that, it will only need a simple setting added
to it. Make sure you are logged in to Facebook and go to:
https://developers.facebook.com/
Select your app from the My Apps dropdown in the header nav. Then select settings.
427
App Domains
Obviously, I scratched out my App Id. Your App Id and Secret should be there from when we created
it in chapter 10. Also, make sure you have added your id and secret to the config in components.
And thats it, we should be good.
Github App
Next well make an app for Github. Login to Github and go to:
Click on the Register new application button. Then fill in your details as follows:
428
Once you submit, it will return your client id and client secret:
Obviously, yours will display the actual id and secret, not the placeholder text. Also, make sure you
have added your id and secret to the config in components. Ok, so by now you can see how this
works and what info you need to supply.
429
Google App
Im going to provide screenshots, but please note that things may change over time. If what you see
on Google is different than what you see here, this should still be enough to point you in the right
direction. When you go to Google:
https://console.developers.google.com/project
You will see:
google Projects
Then select Credentials under APIs & auth on the left menu. It will bring up:
430
Configure Consent
Continue to:
431
Consent Screen
432
Create Client Id
433
434
I purposely left out the actual Client Id, but you will see it when you land on this page. Please note
the email address is in between ClientId and Client Secret, so dont get confused and grab the wrong
id.
Next click on APIs, you will get:
Google APIs
Select Google+ Api.. Enable the API and you will get:
435
Google APIs
Go back to the APIs screen and enable Google contacts API, its under the Google Apps APIs heading.
And you should be good to go with Google.
LinkedIn App
This provider has significantly less steps than Google. Start by going to:
LinkedIn:
https://www.linkedin.com/secure/developer
You will land on:
Click on Add New Application and you will get the form:
436
A couple of things to note. You can only select one default scope, it will return a cryptic error if you
select more than one.
Also note that the redirect url is the same format as the one for Google, but obviously with linkedin
as the authclient.
You will go to the success page. You want the following:
Consumer Key / API Key maps to clientId
Consumer Secret / Secret Key maps to clientSecret
437
Its important to use clientId and clientSecret in your common/config/main.php settings. And thats
it, you should be good to go.
To get a sense of how we will use social login and register, I will provide some screenshots, followed
by Gists of the related code.
New index.php
Ok, so you can see we have some nice icons for the providers. These are appearing because we have
used the AuthChoice::widget:
echo yii\authclient\widgets\AuthChoice::widget([
'baseAuthUrl' => ['site/auth'],
'popupMode' => false,
])
This widget will call as many icons as you have configured in your components array. Since we
included 5 there, we are getting that many in our view. If you want less icons to appear, remove the
providers from the config. Even though not all are working now, Im keeping them because I will
include them once they are working.
I also made a big redundant Facebook Signup or Login button. Obviously its up to you to make this
look they way you want it to. I put the large facebook button in as well as the widgets to demonstrate
both.
438
Here is what we have changed in the index.php view file, replacing everything up to the opening
php tag of the first collapse widget:
Gist:
New Index.php code
From book:
<?php
use \yii\bootstrap\Modal;
use kartik\social\FacebookPlugin;
use \yii\bootstrap\Collapse;
use \yii\bootstrap\Alert;
use yii\helpers\Html;
use components\FaqWidget;
/* @var $this yii\web\View */
$this->title = 'My Yii Application';
?>
<div class="site-index">
<div class="jumbotron">
<?php
if (Yii::$app->user->isGuest) {
echo yii\authclient\widgets\AuthChoice::widget([
'baseAuthUrl' => ['site/auth'],
'popupMode' => false,
]);
}
?>
<h1>Yii 2 Start <i class="fa fa-plug"></i></h1><br>
<?php
if (!Yii::$app->user->isGuest) {
echo '<p class="lead">Use this Yii 2 Template to start Projects.</p>';
} else {
439
?>
</div>
Youll note that we wrapped our widget in an if statement to test to see if the user is a guest. Then
we do another if statement to check for guest and if the user is a guest, show them the facebook sign
up or sign in button.
440
New index.php
You can see that all we did was strategically place the widget. No need to review the code, but I will
supply the gist for the entire file for reference:
Gist:
Login
441
New index.php
Again, such a simple change we dont need to review, but here is the Gist for reference:
New Signup View
442
Social Sync
]];
We are specifying in the url to go to site controller, auth action, and then handing it a get parameter
of authclient=facebook for example.
We have no auth action yet, but we will take care of that soon.
In order for font-awesome to work correctly, we have to specify encodeLabels to false in the
Nav::widget:
443
echo Nav::widget([
'options' => ['class' => 'navbar-nav navbar-right'],
'items' => $menuItems,
'encodeLabels' => false,
]);
Obviously, string is not a data type that MySql will recognize. Plus, using dashes in the foreign key
seemed to create a problem for me, so I had to use MySql Workbench to get this syntax correct for
that and other issues. This is what it gave me:
Gist:
SQL for Auth table
From book:
CREATE TABLE IF NOT EXISTS `yii2build`.`auth` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT(11) UNSIGNED NOT NULL,
`source` VARCHAR(255) NOT NULL,
`source_id` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`),
INDEX `fk_auth_user_id_user_id` (`user_id` ASC),
CONSTRAINT `fk_auth_user1`
FOREIGN KEY (`user_id`)
REFERENCES `yii2build`.`user` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
444
I thought it would be wise to have an example in SQL that I could refer to in the future, if I needed
to. If you are collaborating with someone or need to post code on Github or elsewhere, you will end
up writing the SQL or creating a migration.
Auth Model
Now that we have the table, the next step is to create an Auth model. We going to do this using Gii:
Auth Model
Were going to put the model in common, so the namespace field is:
common\models
Then we just generate the file and were good to go. Gii will automatically include the relationship
we need:
public function getUser()
{
return $this->hasOne(User::className(), ['id' => 'user_id']);
}
445
You can see that auth is specifying the class and a successCallback parameter, essentially telling
the actions that when the url is site/auth on a callback to use the onAuthSuccess method.
OnAuth Success
We will add that method to the site controller shortly. First lets look at the onAuthSuccess method
from the guide:
446
447
$transaction->commit();
Yii::$app->user->login($user);
} else {
print_r($auth->getErrors());
}
} else {
print_r($user->getErrors());
}
}
}
} else { // user already logged in
if (!$auth) { // add auth provider
$auth = new Auth([
'user_id' => Yii::$app->user->id,
'source' => $client->getId(),
'source_id' => $attributes['id'],
]);
$auth->save();
}
}
}
}
That will work to some degree for Facebook. You can test the basic signup and login, it should work.
But its not quite right for our purposes.
Im going to step us through the onAuthSuccess method in detail, but there are enough changes in
my version that I think we should just get straight to it.
448
$username = 'name';
}
$auth = Auth::find()->where([
'source' => $source,
'source_id' => $attributes['id'],
])->one();
if (Yii::$app->user->isGuest) {
if ($auth) { // login
$user = $auth->user;
Yii::$app->user->login($user);
} else { // signup
if (isset($attributes['email'])
&& User::find()->where
(['email' => $attributes['email']])->exists()) {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "User with the same email as in {client} account
already exists but isn't synced. Login with username and password
and click the {client} sync link to sync accounts.",
['client' => $client->getTitle()]),
]);
} else {
$password = Yii::$app->security->generateRandomString(6);
$user = new User([
'username' => $attributes[$username],
'email' => $attributes['email'],
'password' => $password,
]);
$user->generateAuthKey();
//$user->generatePasswordResetToken();
$transaction = $user->getDb()->beginTransaction();
if ($user->save()) {
449
450
Yii::$app->getSession()->setFlash('success', [
Yii::t('app', "Your {client} account is successfully
synced.", ['client' => $client->getTitle()]),
]);
Note that I have not added Google and LinkedIn yet. I will do this in the final version.
451
452
Its taking an object of the auth provider, which will hold all the attributes, which we can access like
so:
$attributes = $client->getUserAttributes();
453
// debug stuff
//$attributes['email'] = null;
//var_dump($this->attributes);
// die();
If you want to test what happens if email is not set in $attributes, uncomment:
$attributes['email'] = null;
Just make sure you comment out the rest of the method or it will not pause for you to see the results.
So Facebook, for example, returns the following:
array(11) { ["id"]=> string(15) "11111111111111111"
["email"]=> string(17) "[email protected]"
["first_name"]=> string(4) "Bill"
["gender"]=> string(4) "male"
["last_name"]=> string(4) "Keck"
["link"]=> string(60)
"https://www.facebook.com/app_scoped_user_id
/1111111111111111/" ["locale"]=> string(5) "en_US"
["name"]=> string(9) "Bill Keck"
["timezone"]=> int(-0)
["updated_time"]=> string(24) "2015-02-18T02:05:22+0000"
["verified"]=> bool(true) }
Obviously I have changed some of the values not to expose personal data.
Ok, comment out the debug and uncomment the rest of the method.
The first thing we do after creating $attributes to hold our values is check to see if they have provided
an email:
454
if (!isset($attributes['email'])){
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "Unable to finish, {client}
did not provide us with an email.
Please check your settings on {client}.",
['client' => $client->getTitle()]),
]);
By using the Yii::t method, we can create a client token, using the brackets, and set the token value,
in this case:
['client' => $client->getTitle()])
Next we set up a switch statement to allow us to set attributes according to provider, since they have
different names for things:
$source = $client->getId();
switch ($source){
$username = 'name';
break;
455
$username = 'login';
break;
$username = 'screen_name';
break;
default:
$username = 'name';
$client->getId() gives us the name of the provider, so we assign this to a local variable named $source
and then switch on that.
We are also using $username as variable name that will hold the attribute value we are looking for.
You can see that with our 3 providers listed, we have 3 different original field names that we convert
to $username.
We are using $source and $username later in the code.
Next we try to grab an auth record for the user if it exists:
456
$auth = Auth::find()->where([
])->one();
If you recall our data structure for the auth table, we record a source_id, which is the id record
the social provider gives them. So using, Facebook as an example, if there is an auth record with a
source_id that matches the id from the $attributes array, then we have a matching record for that
user.
Next it will try to login in the user:
if (Yii::$app->user->isGuest) {
if ($auth) { // login
$user = $auth->user;
Yii::$app->user->login($user);
If we have successfully returned a record from the previous code block, we log the user in. If not,
we go to the else statement. Lets look at the first part:
// signup
if (isset($attributes['email']) && User::find()->where
(['email' => $attributes['email']])->exists()) {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "User with the same email as in {client}
account already exists but isn't synced. Login with
username and password and click the {client} sync
link to sync accounts.", ['client' => $client->getTitle()]),
]);
}
457
We do a check to see if the email is already taken. Were using the handy exists method, so take note
of that for future reference. If it does already exist, we return a flash message with the appropriate
tokens.
If we do not already have the email in our db, then we execute the else statement, which will create
the users account. We start by generating a password:
else {
$password = Yii::$app->security->generateRandomString(6);
$user = new User([
'username' => $attributes[$username],
'email' => $attributes['email'],
'password' => $password,
]);
$user->generateAuthKey();
//$user->generatePasswordResetToken();
Then you can see we create a new instance of User and set the column values we need for the user
record from the $attributes array and the newly created $password. You can see I commented out:
//$user->generatePasswordResetToken();
Im just keeping that for reference since it was in the method in the guide. Its completely
unnecessary for our implementation, so you can remove it if you want.
Since we are saving records into two tables, one for auth and one for user, we will use a transaction.
We start like this:
$transaction = $user->getDb()->beginTransaction();
Then we move on to saving the user and creating a new auth record and saving that as well:
458
if ($user->save()) {
$auth = new Auth([
'user_id' => $user->id,
'source' => $client->getId(),
'source_id' => (string)$attributes['id'],
]);
if ($auth->save()) {
$transaction->commit();
Yii::$app->user->login($user);
MailCall::onMailableAction('signup', 'site');
}
You can see that if we save both the $user and $auth, we commit the transaction. Then we login the
user and send them an autoresponder.
Im basing this use of a transaction on the original example of this method that was provided in the
guide. Normally, I would expect to see a try/catch block and a rollback statement, but that doesnt
seem to be necessary here.
I did play around with it to see if I could break it and get one of the pieces of the transaction to save,
but from what I can tell it actually enforces the transaction as written.
I checked the guide:
Yii 2 Database
But I couldnt find any reference to the way we are doing it here. So, since it works, it came from
another part of the guide, and it allows me to write error messages with tokens, Im going to use it.
That brings us to the next part. If the transaction does not complete, we send them a message:
else {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "We were unable to complete the
process and sync {client}.",
['client' => $client->getTitle()]),
]);
}
} else {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "We were unable to complete the process
and sync {client}.",
['client' => $client->getTitle()]),
459
]);
}
}
}
You would think were done, but of course were not. Now we execute on the else statement when
we checked on whether or not the user was a guest. So, if they are not a guest, they are already
logged in and we move on:
else { // user already logged in
if (!$auth && $this->attributes['email'] ==
Yii::$app->user->identity->email) { // add auth provider
$auth = new Auth([
'user_id' => Yii::$app->user->id,
'source' => $source,
'source_id' => (string)$attributes['id'],
]);
$auth->save();
Yii::$app->getSession()->setFlash('success', [
Yii::t('app', "Your {client} account
is successfully synced.",
['client' => $client->getTitle()]),
]);
Since we have previously tried to return an instance of $auth where source_id and $attributes[id]
match, we can use an if statement set the condition to create an auth record:
if (!$auth && $this->attributes['email'] == Yii::$app->user->identity->email)
So if that evaluates true, then create the new auth record. Note that the email values have to match.
Youll notice (string) here:
'source_id' => (string)$attributes['id'],
Thats there because we need to tell it we want a string, which is what we need to pass validation.
Remember, we set that column up as a varchar, and this gives us flexibility to accommodate the
different formats from the providers.
Then finally, we have:
460
}
}
}
So were covered in case the emails dont match or if the account was already synced.
This is a working solution and you should be able to test it successfully. But theres a problem, which
might not seem so obvious at first. As subtle as it is, we are running into code duplication.
Notice that we used:
MailCall::onMailableAction('signup', 'site');
In the signature, signup designates the action, but that is not the correct action. So we would either
have to make another status message for this action, which makes no sense, or we have to use this
onMailableAction in a way that was not intended.
That workaround is easy enough, and just to get up and running and test things, its fine. But
maintaining an application over time, this could be a real problem. If you made changes to how
and when you want to call the same autoresponder, you would have to update in 2 places. Thats
just a horrible practice if it can be avoided.
And the autoresponder isnt the only code duplication. Login and signup should be handled by their
respective controller actions. By creating methods independent of those actions, you leave the door
open to coding errors and omissions. If you want to do things like add session data to the user login
461
process or block certain ip address from registration, you would again have to do it in 2 separate
places. Not good.
Wouldnt it be better to move the sign up and sign in portions of onAuthSuccess into those respective
actions? My view is that we should. It will make the actionLogin and actionSignup more complicated,
but there is no way to avoid that if we are going to have it all in one method. This approach is entirely
optional however and you are free to do as you choose.
createUser()
createAuth($user)
findExistingAuth()
emailPresent()
matchEmail()
formatProviderResponse($source)
emailAlreadyInUse()
Some of those methods are just for cosmetic readability and some are to cut down on duplication. I
wanted to make it easy to come back in the future and understand the code.
Dont worry, we are going to work on this incrementally, so you will understand it fully.
In order to move the values around to more than one method, we need to set them as class properties.
$attributes = [];
$username;
$source;
$socialUser;
462
//$this->attributes['email'] = null;
//var_dump($this->attributes);
// die();
default:
$this->username = 'name';
}
$auth = Auth::find()->where([
'source' => $this->source,
'source_id' => $this->attributes['id'],
])->one();
if (Yii::$app->user->isGuest) {
if ($auth) { // login
$user = $auth->user;
Yii::$app->user->login($user);
} else { // signup
if (isset($this->attributes['email'])
&& User::find()->where
(['email' => $this->attributes['email']])->exists()) {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "User with the same email as in {client}
account already exists but isn't synced. Login with
username and password and click the {client} sync link
to sync accounts.", ['client' => $client->getTitle()]),
]);
} else {
$password = Yii::$app->security->generateRandomString(6);
$user = new User([
'username' => $this->attributes[$this->username],
'email' => $this->attributes['email'],
'password' => $password,
]);
$user->generateAuthKey();
//$user->generatePasswordResetToken();
463
$transaction = $user->getDb()->beginTransaction();
if ($user->save()) {
$auth = new Auth([
'user_id' => $user->id,
'source' => $client->getId(),
'source_id' => (string)$this->attributes['id'],
]);
if ($auth->save()) {
$transaction->commit();
Yii::$app->user->login($user);
MailCall::onMailableAction('signup', 'site');
} else {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "We were unable to complete the process
and sync {client}.", ['client' => $client->getTitle()]),
]);
}
} else {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "We were unable to complete the process
and sync {client}.", ['client' => $client->getTitle()]),
]);
}
}
}
} else { // user already logged in
if (!$auth && $this->attributes['email'] ==
Yii::$app->user->identity->email) { // add auth provider
$auth = new Auth([
'user_id' => Yii::$app->user->id,
'source' => $this->source,
'source_id' => (string)$this->attributes['id'],
]);
464
465
$auth->save();
Yii::$app->getSession()->setFlash('success', [
Yii::t('app', "Your {client} account is
successfully synced.",
['client' => $client->getTitle()]),
]);
} else {
if($this->attributes['email'] != Yii::$app->user->identity->email){
Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "Your {client} account could not be synced.",
['client' => $client->getTitle()]),
]);
} else { // account was already synced
Yii::$app->getSession()->setFlash('success', [
Yii::t('app', "Your {client} account is already synced.",
['client' => $client->getTitle()]),
]);
}
}
}
}
So now you can test that and it should all be good. Making incremental modifications makes it easier
to make the changes we need.
466
So you can see that we simply chopped out the code from the onAuthSuccess method and made it a
private function. We generate a password, create a new instance of User, and generate an auth key,
and then return.
Now we can call it like so:
$user = $this->createUser();
You will see that in action shortly and hopefully you will like what it does to the readability of the
code.
Next we have createAuth:
gist:
createAuth
From book:
467
Again we simply chopped it out of our onAuthSuccess and made it a private function. Since we need
to set user_id $user->id, we need to hand in an instance of User in the signature.
Next we have findExistingAuth:
Gist:
findExistingAuth
From book:
private function findExistingAuth()
{
$auth = Auth::find()->where([
'source' => $this->source,
'source_id' => $this->attributes['id'],
])->one();
return $auth;
}
This one simply finds the existing auth record, if one exists. If it doesnt exist, it returns null.
Next is emailPresent:
Gist:
emailPresent
From book:
468
This one just tells us if the provider has passed along the email.
Next we have matchEmail:
Gist:
matchEmail
From book:
private function matchEmail()
{
return $this->attributes['email'] ==
Yii::$app->user->identity->email ? true : false;
}
Obviously should just be one line. This just tells us if the email returned by the provider matches
the one of the current application user.
Next is formatProviderResponse:
Gist:
formatProviderResponse
private function formatProviderResponse($source)
{
switch ($source){
case $source == 'facebook' :
$this->username = 'name';
break;
case $source == 'github' :
469
$this->username = 'login';
break;
case $source == 'twitter' :
$this->username = 'screen_name';
break;
case $source == 'linkedin' :
$this->username = 'fullName';
$fullName = $this->attributes['first_name'] . ' ' .
$this->attributes['last_name'];
$this->attributes['fullName'] = $fullName;
break;
case $source == 'google' :
$this->username = 'displayName';
$emails = $this->attributes['emails'];
foreach ($emails as $email){
foreach ($email as $k => $v) {
if ($k == 'value'){
$this->attributes['email'] =
}
}
}
break;
default:
$v;
470
$this->username = 'name';
}
This method formats the providers response. As our application grows more sophisticated and we
want to work with more attributes, we can format them here. You can also see how easy it is to add
another provider, just add another case statement as I did for Google and LinkedIn.
Of course both of the providers made it more complicated to format the response. In the case of
Linkedin, I had to create a name out of first name and last name and I called it fullName. I then set
it to the class property, so we could use it later.
In the case of Google, they apparently can give us more than one email, so they did not have a value
named email. That meant I had to extract it from a multidimensional array using nested foreach
loops.
Next we have emailAlreadyInUse:
Gist:
emailAlreadyInUse
From book:
private function emailAlreadyInUse()
{
return User::find()
->where(['email' => $this->attributes['email']])
->exists() ? true : false;
}
It simply tells us if the email returned by the provider is already in use by a registered user, we use
before syncing to determine if the user should login first or not.
Now as we move on, we will use these methods and you will see how they make the code easier to
follow.
OnAuthSuccess Method
Lets start with the onAuthSuccess method, which has been modified drastically:
Gist:
onAuthSuccess
From book:
public function onAuthSuccess($client)
{
$this->attributes = $client->getUserAttributes();
$this->source = $client->getId();
$this->formatProviderResponse($this->source);
if (!$this->emailPresent()){
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "Unable to finish, {source} did not provide us
with an email. Please check your settings on {source}.",
['source' => $this->source]),
]);
}
$existingAuth = $this->findExistingAuth();
if (Yii::$app->user->isGuest) {
if ($existingAuth) { // login steps
$this->socialUser = $existingAuth->user;
$viaSocial = true;
$this->actionLogin($viaSocial);
471
Yii::$app->getSession()->setFlash('success', [
Yii::t('app', "Your {source} account is successfully synced.",
['source' => $this->source]),
]);
} else { //emails don't match
if(!$this->matchEmail()){
Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "Your {source} account could not be synced.",
['source' => $this->source]),
]);
} else { // account was already synced
Yii::$app->getSession()->setFlash('success', [
Yii::t('app', "Your {source} account is already synced.",
['source' => $this->source]),
]);
}
}
}
}
Yes, its still a beast, but a lot more readable. Lets look at the first part:
472
473
So, just like before, we are pulling in the UserAttributes from the $client object which has been
handed in, and we set them to our class property $this->attributes, which is an array which will
hold the values.
Then we set the $source property from $client->getId(). This is followed by the formatProviderResponse method, which will look at the $source and set $this->username accordingly.
Then comes the if statement to see if the email is present:
if (!$this->emailPresent()){
Thats just way easier to understand, so when I come back to it a year from now, I will save time by
being able to comprehend it more quickly.
If that statement evaluates true and we dont have an email, we return a flash message informing
the user that they need to check their providers settings.
Since we have $this->source available to us as a class property and it has already been set, we are
using that instead of $client->getTitle().
Ok, moving on:
$existingAuth = $this->findExistingAuth();
This will return null if there is no existing auth record or it will return the right record if it exists.
We are going to use this in a check in our next block:
474
if (Yii::$app->user->isGuest) {
if ($existingAuth) { // login steps
$this->socialUser = $existingAuth->user;
$viaSocial = true;
$this->actionLogin($viaSocial);
So we check to see if the user is a guest, then test to see if the $existingAuth evaluates true, if so, we
want to login, if not, we want to signup.
Since $existingAuth is an instance of Auth, and since we have a relation to User in the Auth model,
we can access the user via:
$existingAuth->user;
So we set this user to the class property $socialUser, so we will have access to it in actionLogin:
$this->socialUser = $existingAuth->user;
Then we set $viaSocial to true, which as youll see in a moment, has meaning in the actionLogin
method, which we call:
$this->actionLogin($viaSocial);
Action Login
Now lets take a look at the actionLogin method:
Gist:
actionLogin
From book:
475
This actually didnt change too much. We obviously have set $viaSocial=null as a default in the
signature. This allows us to not hand in a value for $viaSocial when we login in through the normal
login form.
Then first thing after checking to see if the user is already logged in, we test for $viaSocial:
if ($viaSocial){
Yii::$app->user->login($this->socialUser);
} else
So if $viaSocial is true, we login the user by handing in $this->socialUser into the login method of
the user class, which is not the same as actionLogin on the controller. We covered that earlier in the
book.
Weve already set $this->socialUser as a class property earlier in the onAuthSuccess method, so it is
available to us here.
476
Everything after the else statement is exactly what was there before, so you can see our login method
hasnt changed too drastically.
The actionSignup method, however, is quite a bit bigger.
Action Signup
Gist:
actionSignup
From book:
public function actionSignup($viaSocial=false)
{
if ($viaSocial){
if ($this->emailPresent() && $this->emailAlreadyInUse()) {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "User with the same email as in {source} account
already exists but isn't synced. Login with username and
password and click the {source} sync link to sync accounts.",
['source' => $this->source]),
]);
} else {
$user = $this->createUser();
$transaction = $user->getDb()->beginTransaction();
if ($user->save()) {
$auth = $this->createAuth($user);
if ($auth->save()) {
$transaction->commit();
Yii::$app->user->login($user);
MailCall::onMailableAction('signup', 'site');
} else {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "We were unable to complete the process
and sync {source}.", ['source' => $this->source]),
]);
}
} else {
if( User::find()->where(['username' => $this->username])){
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "Username already taken, please signup
through the site Singup form and use a different
username, thanks."),
]);
} else {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "We were unable to complete the process
and sync {source}.", ['source' => $this->source]),
]);
}
}
}
}
else {
$model = new SignupForm();
if ($model->load(Yii::$app->request->post())) {
if ($user = $model->signup()) {
if (Yii::$app->getUser()->login($user)) {
477
478
MailCall::onMailableAction('signup', 'site');
return $this->goHome();
}
}
}
return $this->render('signup', [
'model' => $model,
]);
}
}
Breaking this down into smaller chunks makes it easier to digest. Lets take the first part:
public function actionSignup($viaSocial=false)
{
if ($viaSocial){
if ($this->emailPresent() && $this->emailAlreadyInUse()) {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "User with the same email as in {source} account
already exists but isn't synced. Login with username and
password and click the {source} sync link to sync accounts.",
['source' => $this->source]),
]);
} else
So we use the same technique of setting $viaSocial to false as a default so that we can use the regular
signup form if we wish to without having to pass a value in. Then we test for:
if ($this->emailPresent() && $this->emailAlreadyInUse()) {
You can see thats easy to understand. If we evaluate true there, we return a flash message, else:
$user = $this->createUser();
$transaction = $user->getDb()->beginTransaction();
if ($user->save()) {
$auth = $this->createAuth($user);
if ($auth->save()) {
$transaction->commit();
Yii::$app->user->login($user);
MailCall::onMailableAction('signup', 'site');
} else {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "We were unable to complete the process
and sync {source}.", ['source' => $this->source]),
]);
}
} else {
if( User::find()->where(['username' => $this->username])){
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "Username already taken, please signup
through the site Singup form and use a different
username, thanks."),
]);
} else {
return Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "We were unable to complete the process
and sync {source}.", ['source' => $this->source]),
]);
}
479
480
}
}
Note in the above, we do a comparison to check and see if the registration is failing due the username
already being in use, since validation requires it to be unique. If it is already in use, the user gets a
nicely formatted message instructing them on what to do.
We start the else statement by creating a user via our handy createUser method:
$user = $this->createUser();
You can see we use our createAuth method with $user handed in, so the code here is a bit more
concise than it would otherwise be.
We follow that with 2 else statements that return a flash message if something goes wrong and the
transaction does not complete.
That brings us the final else statement of the method, and that is simply everything we had before
we made the change, so we can either process form input or render the form.
Since were in the signup action for example, you can see that now the following call makes sense:
MailCall::onMailableAction('signup', 'site');
That was a lot of work for a small effect. However, imagine that you are also doing other things
nearby:
481
MailCall::onMailableAction('signup', 'site');
Those are just a couple of examples as comments only, but you get the idea. As your application
grows, you will want a single centralized method to add these requirements to.
Ok, that was a huge, but necessary detour. Now we are ready to return to onAuthSuccess. If we are
not signing up or logging in, then we are already logged in:
else { // user already logged in, require email match
if (!$existingAuth && $this->matchEmail()) { // add auth provider
$auth = $this->createAuth(Yii::$app->user);
$auth->save();
Yii::$app->getSession()->setFlash('success', [
Yii::t('app', "Your {source} account is successfully synced.",
['source' => $this->source]),
]);
We check to see if there is no $existingAuth and that the email matches. If were good, we createAuth,
handing in the currently logged in user and save. We send them a nice confirmation message.
If were not good, we have the else statement:
else { //emails don't match
if(!$this->matchEmail()){
Yii::$app->getSession()->setFlash('error', [
Yii::t('app', "Your {source} account could not be synced.",
['source' => $this->source]),
]);
} else { // account was already synced
Yii::$app->getSession()->setFlash('success', [
482
So now we are just handling if the emails dont match or if the account is already synced. Some
people just love pushing a button just to see what happens, so we have to handle that scenario.
Now all of that should be working and testable. If you go over the code, you will find it much more
readable, and therefore, it should be more maintainable.
So one negative effect of all this is that our site controller is a lot more crowded with code than
it used to be. The inclusion of social auth required 8 new methods and a rewrite of two existing
methods.
For my taste, this is a little too much in one controller, if such a thing can be avoided. So I decided
to move actionIndex, actionContact and actionAbout to a new controller, which Im going to name
PagesController.
Any additional static pages, such as terms of service, privacy, etc. will be controlled by the
PagesController.
Pages Controller
Im not going to cover every detail in this modification to the template because you already know
how to do most of it. However I will make a list of steps, along with any key details, so that you
dont miss anything and I also provide Gists at the end of the chapter.
Steps to implement:
1. Create frontend pages controller via Gii with index, about, and contact actions.
2. Include the following on the pages controller:
namespace frontend\controllers;
use Yii;
use frontend\models\ContactForm;
use yii\filters\AccessControl;
1. Move index view, about view, and contact view from site views folder to pages views folder.
2. Change facebook button link on pages index to point to site/auth.
3. Configure captcha correctly. You will need to put in the following behaviors method on the
pages controller:
483
1. Configure default controller/action for application because the new site controller will have
the following actions when the change is done:
actions
login
logout
signup
RequestPasswordReset
484
ResetPassword
Additionally, it has the following methods:
behaviors
onAuthSuccess
createUser
createAuth
findExistingAuth
emailPresent
matchEmail
formatProviderResponse
emailAlreadyInUse
Obviously, if we removed index from the site controller, the application will need to know where to
send users. We have some choices. We need to set the following:
'defaultRoute' => 'pages/index',
485
So you have to weigh the convenience vs. the potential security issue. In my case, I chose not to
implement social auth on the backend. That is subject to change of course, but thats how Im doing
it for now.
Ok, so for reference, and in case you need it for debugging, Im going to provide the Gists for the
new site controller and pages controller.
Gist:
Site Controller
Gist:
Pages Controller
Summary
We implemented one-click Facebook login and registration, covering every scenario we could think
of. We used Yii 2s authclient extension, the authclient widget, and made significant changes to the
site controller.
It was important for us to make sure actionLogin and actionSignup on the site controller would be
maintained properly, so we had to integrate our social auth into those methods as well.
Then finally we moved the rest of the site controller actions, like about, contact, privacy, etc. to a
new controller, pages. Our pages controller will take care of all the site pages that dont otherwise
have their own controllers, including index.
We decided that in our application, we would not make the same changes to the backend application.
This is a personal choice and you may choose to do otherwise if you wish.
Once again Id like to thank all the readers for their contributions and suggestions, help in finding
typos and bugs, and of course for positive reviews and recommendations.
Please feel free to contact me at leanpub.com through the contact the author link:
Email Bill Keck
Thanks again for supporting the book.
Although this worked and did not create problems in the code, a number of programmers were
confused by it, since the more typical relationship would look like:
public function getRole()
{
return $this->hasOne(Role::className(), ['id' => 'role_id']);
}
After receiving a letter from a University student in Germany asking about the relationship, I decided
to review the code with some ninja programmers that I know. They too were uncomfortable with
the existing relationship.
487
So I realized that this would impact maintainability of the template because its not as intuitive as it
should be. It all worked fine, but was probably not the best practice. And because Im building this
template for long term use, I couldnt ignore this fact.
Rather than be a negative, the situation produced a number of upsides. First, we get cleaner, more
intuitive code, which makes the template stronger.
It also gave me a chance to revisit old code and an opportunity to go deeper into Yii 2s Active Record
implementation, which is very powerful and made the rewrite enjoyable. I got to demonstrate how
easy Yii 2 makes queries, whereas before I was simply relying on raw SQL.
The following models are changed:
User
Gist:
New User Model
Note: This changes assumes you have a status record in your DB with a status_name of Active and
is also dependent on you making the rest of the changes in this chapter. This version of User is used
from chapter 7 on. If you want the earlier version, please refer to the Gist at the end of chapter 5.
Role
Gist:
New Role Model
UserType:
Gist:
New UserType Model
Status
Gist:
New Status Model
ValueHelpers
Gist:
New ValueHelpers Class
488
PermissionHelpers
Gist:
New PermissionHelpers Class
RecordHelpers
Gist:
New RecordHelpers Class
Database Changes
We need to change the defaults on the user table. Also, in the DB itself, the columns role_id, user_type_id, and status should default to 1.
This assumes that in your DB, the first record with an id of 1 in your role table is role_name User.
Also, in the status table, the first record with an id of 1, is Active. And finally in the user_type table,
the first record with an id of 1, is Free.
Alternatively, you can drop the defaults in the db, and in the rules method, use:
['status_id', 'default', 'value' => ValueHelpers::getStatusId('Active')],
['role_id', 'default', 'value' => ValueHelpers::getRoleId('User')],
['user_type_id', 'default', 'value' => ValueHelpers::getUserTypeId('Free')],
Note that using the ValueHelpers method to set the default for status is something we do in chapter
7, which allows us to remove the constant at the top of the User model. Again make sure you have
a record with a name of Active in the status table before making this change.
Extra ValueHelpers
I dont supply the getUserTypeId and getRoleId methods out of the box, so you will have to add to
ValueHelpers, if you wish to remove the hardcoded defaults from the db:
Gist:
Extra ValueHelpers
From book:
489
It might be a good idea to add the above methods just to have them, even if you dont immediately
need them.
LoginForm Model
In chapter 7, we change the loginAdmin method on the LoginForm model located in common/models
to the following:
Gist:
New LoginForm Model
PasswordResetRequestForm
We also made changes to the frontend/models/PasswordResetRequestForm.php:
Gist:
New PasswordResetRequestForm Model
In chapter 11, we make a correction to the UserSearch model and the ProfileSearch model, which
corrects a bug.
490
UserSearch
Gist:
UserSearch Model
ProfileSearch
Gist:
ProfileSearch Model
Also, in chapter 11, we make changes to:
backend/views/layouts/main.php
and to:
backend/views/site/index.php.
The versions Im going to provide here are the final versions as of chapter 14. If you wish to pull in
the versions from chapter 11, please refer to the gists in that chapter.
Main.php
Gist:
Main.php
Note: This is the current version as of chapter 14. If you want the earlier version, refer to chapter 11.
Gist:
Index.php
Index.php
Note: This is the current version as of chapter 14. If you want the earlier version, refer to chapter 11.
Troubleshooting
1. Errors due to method not found. This could be a namespace problem, so make sure you have
your use statements that are necessary for the class. It could also come from referencing a
method whose name has been changed.
491
2. Errors due to missing records from DB. In order for the application to operate correctly, you
need records in the role, status, and user_type tables as outlined in the instructions above.
3. Errors due to view permissions. Since we changed the backend layout/main and site/index
views, these changes will need to be in place before you can use the backend.
Its entirely possible that I missed something, please contact me if that is the case. You can email me
at:
Email Bill Keck
The good news is that I know the template works and the code has gone directly from my IDE to
the Gists, so you should be able to correct anything by following the instructions in the book.
Summary
I cant tell you how many technical books Ive read with broken useless code, but its a lot. Its such
a waste of a programmers time. Im committed to making this book different. The template we
build in this book is a long-term project that will evolve over time, especially as I add more bonus
material.
Thats why I made this chapter to insure that all readers of the book have an easy way to keep the
template up-to-date. Its important to me that you get maximum value out of the book and that we
achieve a high level of excellence with this work.
Thanks again for sharing this journey with me.
493
494
ON DELETE NO ACTION
ON UPDATE NO ACTION)
Note that were sticking with the convention of singular for the table name. You can also see that
we have a foreign key, status_id pointing at id on the status table. Because there are so few records
in the status table, I didnt bother making id unsigned, so that it why status_id is not unsigned as
well.
A couple of other notes about the data structure. We are using marketing_image_is_active, set as a
boolean, to determine if the image should be the active item in the carousel. The marketing_image_caption will be displayed in the carousel.
We are using status_id to determine the status of the image because the status table holds the values
active, pending, and retired. So this gives us a chance to reuse existing data structure and avoid data
duplication.
This data structure is for our template, but you are absolutely free to change it if you have other
requirements. If you want more options to designate different types of images, feel free to do so,
but personally, I would do so after working through the rest of the chapter, so you know how it is
supposed to work first.
By this point, you should be familiar enough with Gii for me not to have to repeat all the instructions.
So I will just assume you have correctly created the MarketingImage model.
Next we need to create the CRUD. Since we already have our search folder in backend/models, we
dont need to create it.
So go ahead and create the CRUD with Gii.
Dont you just love this workflow? Every time I use it Im reminded about how much I enjoy working
with Yii 2.
Gist:
Modified MarketingImage Model
From book:
<?php
namespace backend\models;
use
use
use
use
use
Yii;
yii\db\ActiveRecord;
yii\db\Expression;
yii\helpers\ArrayHelper;
yii\behaviors\TimestampBehavior;
/**
* This is the model class for table "marketing_image".
*
* @property string $id
* @property string $marketing_image_path
* @property string $marketing_image_name
* @property integer $marketing_image_is_featured
* @property integer $marketing_image_is_active
* @property integer $status_id
* @property string $created_at
* @property string $updated_at
*
* @property Status $status
*/
class MarketingImage extends \yii\db\ActiveRecord
{
public $file;
/**
* @inheritdoc
*/
public static function tableName()
{
return 'marketing_image';
}
495
496
497
498
Were going to step this rather quickly and just talk about whats different from the boiler plate.
Lets start with the use statements:
use
use
use
use
use
499
Yii;
yii\db\ActiveRecord;
yii\db\Expression;
yii\helpers\ArrayHelper;
yii\behaviors\TimestampBehavior;
We pulled in the necessary classes to support TimestampBehavior and the ArrayHelper for our
dropdown lists.
Next we added a public property:
public $file;
The reason we have to add it like this is because the model cant pull it from the table via reflection
like it does all the other properties. In this case, $file will hold the value of the file, not a db table
value.
Next we added our timestamp behavior in the behaviors method:
public function behaviors()
{
return [
'timestamp' => [
'class' => TimeStampBehavior::className(),
'attributes' => [
ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
],
'value' => new Expression('NOW()'),
],
];
}
500
We are using a couple of validators that we havent used before. the trim validator gets rid of white
space, but not space in between words. We will write a beforeValidation method for that, since we
want to strictly enforce filenames.
You can also see that we added a validation rule for file:
[['file'], 'file', 'extensions' => ['png', 'jpg', 'gif'],
'maxSize' => 1024*1024],
This sets the allowable types and max size of the file, very simple stuff.
Unfortunately, when I tried to save a photo the first time, I got a nasty error about PHP fileInfo not
being enabled.
PHP FileInfo
For whatever reason, my PHP ini file had it commented out, so I had to get rid of the semicolon:
501
PHP FileInfo
You can see it in the highlighted line above. If you do need to make this change, also remember to
restart Apache so the change takes effect.
Ok, moving on. Lets look at our beforeValidate method:
public function beforeValidate()
{
$this->marketing_image_name =
preg_replace('/\s+/', '', $this->marketing_image_name);
$this->marketing_image_path =
preg_replace('/\s+/', '', $this->marketing_image_path);
return parent::beforeValidate();
}
Were doing formatting here to remove spaces from the marketing_image_path and the marketing_image_name. On the controller, we will do the same for the actual file thats being saved to the server.
That way we dont end up with filenames with spaces in them.
Obviously were just using a preg_replace to replace a regular expression:
/\s+/
502
That will show nicely on the form, when we modify it to accept a file upload. Also note, we use the
magic name for getStatusName.
Next we have a couple of methods that format the values for the dropdown lists we will need in our
form:
public static function getMarketingImageIsFeaturedList()
{
return $droptions = [0 => "no", 1 => "yes"];
}
public static function getMarketingImageIsActiveList()
{
return $droptions = [0 => "no", 1 => "yes"];
}
And finally, we added 2 new status relationships, so we can use status as a dropdown list as well:
public function getStatusName()
{
return $this->status ? $this->status->status_name : '- no status -';
}
/**
* get list of statuses for dropdown
*/
public static function getStatusList()
{
$droptions = Status::find()->asArray()->all();
return ArrayHelper::map($droptions, 'id', 'status_name');
}
We did not need to add getStatus because that was autogenerated for us by Gii due to the foreign
Key definition.
503
Yii;
yii\base\Model;
yii\data\ActiveDataProvider;
backend\models\MarketingImage;
$dataProvider->setSort([
'attributes' => [
'id',
'marketing_image_name',
'marketing_image_path',
'marketing_image_caption',
'marketing_image_is_featured',
'marketing_image_is_active',
'marketing_image_weight',
'statusName' => [
'asc' => ['status.status_name' => SORT_ASC],
'desc' => ['status.status_name' => SORT_DESC],
'label' => 'Status'
],
]
]);
504
505
'id');
'marketing_image_name', true);
'marketing_image_path', true);
'marketing_image_caption', true);
'marketing_image_is_featured');
'marketing_image_is_active');
'marketing_image_weight');
'status_id');
506
/*
* The following line is additionally added for right aliasing
* of columns so filtering happen correctly in the self join
*/
$attribute = "marketing_image.$attribute";
if ($partialMatch) {
$query->andWhere(['like', $attribute, $value]);
} else {
$query->andWhere([$attribute => $value]);
}
}
}
Make sure to set a property for the magic call to relation public $statusName
Make sure to set a rule for $statusName
Make sure $attribute = marketing_image.$attribute; is set correctly
Make sure the correct attributes have the true option on addSearchParameter
Dont expect sort on status to work until we modify gridview in index
507
<?php
use yii\helpers\Html;
use yii\grid\GridView;
use \yii\bootstrap\Collapse;
/* @var $this yii\web\View */
/* @var $searchModel backend\models\search\MarketingImageSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */
$this->title = 'Marketing Images';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="marketing-image-index">
<h1><?= Html::encode($this->title) ?></h1>
<?php
echo Collapse::widget([
'encodeLabels'
'items' => [
=> false,
Search',
=> false,
]
]);
?>
<p>
<?= Html::a('Create Marketing Image', ['create'],
['class' => 'btn btn-success']) ?>
508
</p>
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'id',
'marketing_image_path',
'marketing_image_name',
'marketing_image_caption',
['attribute'=>'marketing_image_is_featured', 'format'=>'boolean'],
['attribute'=>'marketing_image_is_active', 'format'=>'boolean'],
'marketing_image_weight',
'statusName',
// 'created_at',
// 'updated_at',
['class' => 'yii\grid\ActionColumn'],
],
]); ?>
</div>
So by now this should look familiar. We put the search partial inside a collapse widget. One thing
new there is that we set:
'encodeLabels'
=> false,
Search',
This adds a nice touch to the UI. So I went around the application to all the places where I could
make that change to be consistent. Here is the list:
user index
profile index
faq index
faq category Index
The other index pages in the backend dont have the search partial visible. However if you wish to
change that, you can see how easy it would be.
509
Modify View
Ok, moving on. The only other thing to note in this view is that we are using $statusName as one
of the attritubes in the GridView widget and this gives us the eager-loaded sortable attribute.
Lets look at the modified view file:
Gist:
view.php
From book:
<?php
use yii\helpers\Html;
use yii\widgets\DetailView;
/* @var $this yii\web\View */
/* @var $model backend\models\MarketingImage */
$this->title = $model->id;
$this->params['breadcrumbs'][] =
['label' => 'Marketing Images', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="marketing-image-view">
<p>
<?= Html::a('Update', ['update', 'id' => $model->id],
['class' => 'btn btn-primary']) ?>
<?= Html::a('Delete', ['delete', 'id' => $model->id], [
'class' => 'btn btn-danger',
'data' => [
'confirm' => 'Are you sure you want to delete this item?',
'method' => 'post',
],
]) ?>
</p>
<h1><?= Html::encode($model->marketing_image_name) ?></h1>
<br>
510
<div>
<?php
echo Html::img('/'. $model->marketing_image_path . '?'. 'time='. time() ,
['width' => '600px']);
?>
</div>
<br>
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'marketing_image_caption',
'marketing_image_path',
//'marketing_image_name',
['attribute' => 'marketing_image_is_featured',
'format' => 'boolean'],
['attribute' => 'marketing_image_is_active',
'format' => 'boolean'],
'marketing_image_weight',
'status.status_name',
'created_at',
'updated_at',
],
]) ?>
</div>
A couple of minor changes in the DetailView widget and in the H1. Notice the use of:
echo Html::img('/'. $model->marketing_image_path . '?'. 'time='. time() ,
['width' => '600px']);
We are using the Html::img helper to provide the path, then appending a get variable ?time=thecurrent-time. The reason we are appending the get variable is to prevent caching, so we always see
the correct image.
Sometimes where you update the image, you might see the old image if you have it cached in your
browser. Finally, we are enforcing a width to the image, so that we can have all the images set at a
certain size.
511
For our carousel, we might need a specific size, and I will come back to this if that is the case.
Obviously, our view cant display an image yet because we havent uploaded any, but we will be
doing that shortly.
<?= $this->render('_form', [
'model' => $model,
]) ?>
</div>
512
513
You can see we are using model methods to populate the dropdown lists. We did not include
marketing_image_weight on the search form, but obviously you can if you want to.
use yii\widgets\ActiveForm;
use yii\helpers\Html;
/* @var $this yii\web\View */
/* @var $model backend\models\MarketingImage */
/* @var $form yii\widgets\ActiveForm */
?>
<div class="marketing-image-form">
<?php $form = ActiveForm::begin(['options'=>
['enctype' => 'multipart/form-data']]); ?>
<?= $form->field($model, 'marketing_image_name')
->textInput(['maxlength' => 45]) ?>
<?= $form->field($model, 'marketing_image_caption') ?>
<?= $form->field($model, 'marketing_image_is_featured')
->dropDownList($model->marketingImageIsFeaturedList,
['prompt' => 'Please Choose One']);?>
<?= $form->field($model, 'marketing_image_is_active')
->dropDownList($model->marketingImageIsActiveList,
['prompt' => 'Please Choose One']);?>
<?= $form->field($model, 'marketing_image_weight') ?>
<?= $form->field($model, 'status_id')->dropDownList($model->statusList);?>
<?= $form->field($model, 'file')->fileInput(); ?>
<div class="form-group">
<?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update',
['class' => $model->isNewRecord ?
'btn btn-success' : 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end(); ?>
514
515
</div>
This adds the button to upload the file. And if you remember in the model, we set the label to
Marketing Image.
So your form should look like this:
File Form
<?php
namespace backend\controllers;
use
use
use
use
use
use
use
use
use
Yii;
backend\models\MarketingImage;
backend\models\search\MarketingImageSearch;
yii\web\Controller;
yii\web\NotFoundHttpException;
yii\web\ForbiddenHttpException;
yii\filters\VerbFilter;
common\models\PermissionHelpers;
yii\web\UploadedFile;
/**
* MarketingImageController implements the CRUD
* actions for MarketingImage model.
*/
class MarketingImageController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => \yii\filters\AccessControl::className(),
'only' => ['index', 'view','create', 'update', 'delete'],
'rules' => [
[
'actions' => ['index', 'view', 'create', 'update', 'delete'],
'allow' => true,
'roles' => ['@'],
'matchCallback' => function ($rule, $action) {
return PermissionHelpers::requireMinimumRole('Admin')
&& PermissionHelpers::requireStatus('Active');
}
],
],
],
516
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
];
}
/**
* Lists all MarketingImage models.
* @return mixed
*/
public function actionIndex()
{
$searchModel = new MarketingImageSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
/**
* Displays a single MarketingImage model.
* @param string $id
* @return mixed
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}
/**
* Creates a new MarketingImage model.
* If creation is successful, the browser will be redirected to the
517
* 'view' page.
* @return mixed
*/
public function actionCreate()
{
$model = new MarketingImage();
if ($model->load(Yii::$app->request->post())) {
$imageName = $model->marketing_image_name;
$model->file = UploadedFile::getInstance($model, 'file');
$fileName = 'uploads/' . $imageName . '.' . $model->file->extension;
$fileName = preg_replace('/\s+/', '', $fileName);
$model->marketing_image_path = $fileName;
$model->save();
$model->file->saveAs($fileName);
//Save the path in the DB
return $this->redirect(['view', 'id' => $model->id, 'model' => $model,]);
} else {
return $this->render('create', [
'model' => $model,
]);
}
}
/**
* Updates an existing MarketingImage model.
* If update is successful, the browser will be
* redirected to the 'view' page.
* @param string $id
* @return mixed
*/
518
519
520
Ok, so lets discuss what else is different. You can see we added some new use statements and the
following is very important:
use yii\web\UploadedFile;
This gives us access to the UploadedFile class, which we will need to work with our file upload.
521
Also, we obviously modified behaviors to make it compliant with the other backend controllers, so
that it requires admin or greater for access.
After that, its just 3 methods that are different, actionCreate, actionUpdate, and actionDelete.
We create a new instance of MarketingImage, then load the post data if we have any.
Next we create a local variable $imageName to make things easier to work with and assign it like
so:
$imageName = $model->marketing_image_name;
So now we are working with an instance of the actual file and we will be able to assign it the name
we want. But before we save it, we do some formatting to make sure we are stripping any spaces
out of the filename.
So just to make this perfectly clear, we are dealing with an instance of the actual file, which will be
stored in our uploads folder, and also an instance of the model, which we will store in our DB. These
are two separate things.
Once we have made sure the fileName is formatted correctly, we assign it to our $model>marketing_image_path property, then save the model:
$model->marketing_image_path = $fileName;
$model->save();
Its important that we save the model, and therefore perform validation, before we save the the file,
which comes next:
522
$model->file->saveAs($fileName);
Just a tip, if you do that out of order, the instance of the file is no longer available in a temp directory
and it messes up the validation, and you will get an error. So make sure to save model first, then the
file.
The validation Im referring to is in the model:
[['file'], 'file', 'extensions' => ['png', 'jpg', 'gif'],
'maxSize' => 1024*1024],
Yii 2 gives us a simple way to define the allowable extensions and the max size of the file.
So, returning to the controller. After saving, we go to the view file:
return $this->redirect(['view', 'id' => $model->id, 'model' => $model,]);
If we have valid post data, we set a local variable $imageName to the value in the post:
$imageName = $model->marketing_image_name;
Then we need to lookup the existing image name and compare the two:
523
$oldImage = MarketingImage::find('marketing_image_name')
->where(['id' => $id])
->one();
if ($oldImage->marketing_image_name != $imageName){
throw new ForbiddenHttpException
('You cannot change the name, you must delete instead.');
}
We get an instance of the file from post, then save the model. This is useful if we are just changing
something like marketing_image_is_featured from no to yes.
To make an update, you will be required to upload the photo again because the validation rule
requires it.
So we save the new instance of the file and redirect to view:
$model->file->saveAs('uploads/' . $imageName . '.' . $model->file->extension);
return $this->redirect(['view', 'id' => $model->id]);
524
else {
return $this->render('update', [
'model' => $model,
]);
}
We call PHPs unlink method to delete the file and if successful, we also call $model->delete to delete
the model instance from the db.
Then we return to index or we throw an exception because we were unable to complete the unlink.
This method probably should use a try/catch block to return a more useful error. I will make that
improvement in the next section.
And thats it for basic image management. Its not a bad implementation, but we can do better.
For example, when we create an image, we should create a thumbnail to go along with it. Then we
could list that thumbnail in the Gridview and in the other views.
Also, while were able to update a record, the way we have it setup now is that we also have to
re-upload the image or validation fails. But what if you just want to require the file on create, but
not update? That would work out well because it would give you the option of not uploading the
file again if you just wanted to change something like marketing_image_is_featured.
Our solution for that will utilize yii 2s scenario solution, which is pretty cool. I think you will like
it.
525
The downside is that our update method gets a bit more complicated. The upside is that we get a
much more robust solution for updating images.
Ok, lets get started.
Composer Update
Thumbnail Folder
526
Note: I added marketing_image_caption_title to give us more elements to work with in the future,
should we decide to expand our options.
<?php
namespace backend\models;
use
use
use
use
use
use
Yii;
yii\db\ActiveRecord;
yii\db\Expression;
yii\helpers\ArrayHelper;
yii\behaviors\TimestampBehavior;
yii\helpers\Html;
/**
* This is the model class for table "marketing_image".
*
* @property string $id
* @property string $marketing_image_path
* @property string $marketing_image_name
* @property integer $marketing_image_is_featured
* @property integer $marketing_image_is_active
* @property integer $status_id
* @property string $created_at
* @property string $updated_at
*
* @property Status $status
*/
class MarketingImage extends \yii\db\ActiveRecord
{
public $file;
/**
* @inheritdoc
*/
public static function tableName()
{
return 'marketing_image';
}
public function behaviors()
{
527
return [
'timestamp' => [
'class' => TimeStampBehavior::className(),
'attributes' => [
ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
],
'value' => new Expression('NOW()'),
],
];
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['marketing_image_path', 'marketing_image_name',
'marketing_thumb_path','marketing_image_weight'],
'required'],
['marketing_image_weight', 'default', 'value' => 100 ],
['marketing_image_is_featured', 'default', 'value' => 0 ],
['marketing_image_is_active', 'default', 'value' => 0 ],
['file', 'required', 'message' =>
'{attribute} can\'t be blank', 'on'=>'create'],
[['marketing_image_name', 'marketing_image_path'], 'trim'],
[['marketing_image_is_featured',
'marketing_image_is_active', 'marketing_image_weight',
'status_id'], 'integer'],
[['marketing_image_is_featured'],'in',
'range'=>array_keys($this->getMarketingImageIsFeaturedList())],
[['marketing_image_is_active'],'in',
'range'=>array_keys($this->getMarketingImageIsActiveList())],
[['file'], 'file', 'extensions' => ['png', 'jpg', 'gif'],
'maxSize' => 1024*1024],
[['marketing_image_path', 'marketing_image_name'],
'string', 'max' => 45],
[['marketing_image_caption', 'marketing_image_caption_title'],
'string', 'max' => 100],
528
];
}
public function scenarios()
{
$scenarios = parent::scenarios();
$scenarios['create'] = ['file','marketing_image_path',
'marketing_image_name', 'marketing_thumb_path',
'marketing_image_is_featured', 'marketing_image_is_active',
'marketing_image_caption', 'marketing_image_caption_title',
'marketing_image_weight' ];
return $scenarios;
}
public function beforeValidate()
{
$this->marketing_image_name =
preg_replace('/\s+/', '', $this->marketing_image_name);
$this->marketing_image_path =
preg_replace('/\s+/', '', $this->marketing_image_path);
return parent::beforeValidate();
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'marketing_image_path' => 'Marketing Image Path',
'marketing_image_name' => 'Marketing Image Name',
'marketing_thumb_path' => 'Marketing thumb Path',
'marketing_image_caption' => 'Caption',
'marketing_image_caption_title' => 'Caption Title',
'marketing_image_is_featured' => 'Marketing Image Is Featured',
'marketing_image_is_active' => 'Marketing Image Is Active',
'marketing_image_weight' => 'Marketing Image Weight',
'status_id' => 'Status ID',
529
530
531
So lets talk about whats new. We added a use statement for Html:
use yii\helpers\Html;
Well need that when we want to return the url for the thumbnail image in Gridview.
We have a couple of different lines in the rules. We also have a rule for marketing_image_caption_title, but its not shown here:
[['marketing_image_path', 'marketing_image_name',
'marketing_thumb_path'], 'required'],
['file', 'required', 'message' => '{attribute} can\'t be blank',
'on'=>'create'],
You can see we added the new attribute, marketing_thumb_path to the required rule, and moved
required rule for file to a separate line. This is because weve added a condition to the rule via the
on attribute.
'on'=>'create'
This tells the rule to apply only to the create method. As we mentioned, we want the file to be not
required on update because we might only be updating different attributes of the model and not the
image file itself.
532
Scenarios
So logically, the scenario method comes next.
public function scenarios()
{
$scenarios = parent::scenarios();
$scenarios['create'] = [
'file','marketing_image_path',
'marketing_image_name', 'marketing_thumb_path',
'marketing_image_is_featured', 'marketing_image_is_active',
'marketing_image_caption', 'marketing_image_caption_title',
'marketing_image_weight' ];
return $scenarios;
}
Im moving through this quickly, but you should take note of scenarios, this is a powerful feature
that allows us to finesse the rules.
The format here is fairly intuitive. Were setting a scenario for the create method, and we list all the
attributes that will be validated under that scenario. When we want to use this in the controller, we
use it when we call the model:
$model = new MarketingImage();
$model->scenario = 'create';
All in all, very simple. We will see that in action when we work on the controller.
Next we make the additions to our attributeLabels:
'marketing_thumb_path' => 'Marketing thumb Path',
'marketing_image_caption_title' => 'Caption Title',
Lastly, we will add a method to return an image link for our thumbnail, which we will use on our
Gridview:
533
You can see we first get the image and set it to $image using Html::img, then we return it as a link,
using Html::a.
You will love how easy this is to use in Gridview when we come to that part.
Modify MarketingImageSearch
We need to modify MarketingImageSearch, since marketing_image_caption will be a searchable
field.
Gist:
Modified MarketingImageSearch
From book:
<?php
namespace backend\models\search;
use
use
use
use
Yii;
yii\base\Model;
yii\data\ActiveDataProvider;
backend\models\MarketingImage;
/**
* MarketingImageSearch represents the model behind the search form about `backe\
nd\models\MarketingImage`.
*/
class MarketingImageSearch extends MarketingImage
{
public $statusName;
/**
* @inheritdoc
*/
public function rules()
{
return [
[['id', 'marketing_image_is_featured',
'marketing_image_is_active', 'status_id'], 'integer'],
[['marketing_image_path', 'marketing_image_name',
'marketing_image_caption',
'marketing_image_caption_title', 'marketing_image_weight',
'created_at', 'statusName', 'updated_at'], 'safe'],
];
}
/**
* @inheritdoc
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = MarketingImage::find();
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$dataProvider->setSort([
534
535
'attributes' => [
'id',
'marketing_image_name',
'marketing_image_path',
'marketing_image_caption',
'marketing_image_caption_title',
'marketing_image_is_featured',
'marketing_image_is_active',
'marketing_image_weight',
'statusName' => [
'asc' => ['status.status_name' => SORT_ASC],
'desc' => ['status.status_name' => SORT_DESC],
'label' => 'Status'
],
]
]);
if (!($this->load($params) && $this->validate())) {
$query->joinWith(['status']);
return $dataProvider;
}
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
'id');
'marketing_image_name', true);
'marketing_image_path', true);
'marketing_image_caption', true);
'marketing_image_caption_title', true);
'marketing_image_is_featured');
'marketing_image_is_active');
'marketing_image_weight');
'status_id');
}]);
return $dataProvider;
}
protected function addSearchParameter($query, $attribute, $partialMatch = false)
{
if (($pos = strrpos($attribute, '.')) !== false) {
$modelAttribute = substr($attribute, $pos + 1);
} else {
$modelAttribute = $attribute;
}
$value = $this->$modelAttribute;
if (trim($value) === '') {
return;
}
/*
* The following line is additionally added for right aliasing
* of columns so filtering happen correctly in the self join
*/
$attribute = "marketing_image.$attribute";
if ($partialMatch) {
$query->andWhere(['like', $attribute, $value]);
} else {
$query->andWhere([$attribute => $value]);
}
}
}
536
<?php
namespace backend\controllers;
use
use
use
use
use
use
use
use
use
use
Yii;
backend\models\MarketingImage;
backend\models\search\MarketingImageSearch;
yii\web\Controller;
yii\web\NotFoundHttpException;
yii\web\ForbiddenHttpException;
yii\filters\VerbFilter;
common\models\PermissionHelpers;
yii\web\UploadedFile;
yii\imagine\Image;
/**
* MarketingImageController implements the CRUD actions for
* MarketingImage model.
*/
class MarketingImageController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => \yii\filters\AccessControl::className(),
'only' => ['index', 'view','create', 'update', 'delete'],
'rules' => [
[
'actions' => ['index', 'view', 'create',
'update', 'delete'],
'allow' => true,
'roles' => ['@'],
'matchCallback' => function ($rule, $action) {
return PermissionHelpers::requireMinimumRole('Admin')
&& PermissionHelpers::requireStatus('Active');
}
],
],
537
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
];
}
/**
* Lists all MarketingImage models.
* @return mixed
*/
public function actionIndex()
{
$searchModel = new MarketingImageSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
/**
* Displays a single MarketingImage model.
* @param string $id
* @return mixed
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}
538
/**
* Creates a new MarketingImage model.
* If creation is successful, the browser will be redirected to
* the 'view' page.
* @return mixed
*/
public function actionCreate()
{
$model = new MarketingImage();
$model->scenario = 'create';
if ($model->load(Yii::$app->request->post())) {
$imageName = $model->marketing_image_name;
$model->file = UploadedFile::getInstance($model, 'file');
$fileName = 'uploads/' . $imageName . '.' . $model->file->extension;
$fileName = preg_replace('/\s+/', '', $fileName);
$thumbName = 'uploads/' . 'thumbnail/' . $imageName
. 'thumb.' . $model->file->extension;
$thumbName = preg_replace('/\s+/', '', $thumbName);
$model->marketing_image_path = $fileName;
$model->marketing_thumb_path = $thumbName;
$model->save();
$model->file->saveAs($fileName);
Image::thumbnail( $fileName , 60, 60)
->save($thumbName, ['quality' => 50]);
return $this->redirect(['view', 'id' => $model->id, 'model' => $model,]);
} else {
return $this->render('create', [
'model' => $model,
539
]);
}
}
/**
* Updates an existing MarketingImage model.
* If update is successful, the browser will be
* redirected to the 'view' page.
* @param string $id
* @return mixed
*/
public function actionUpdate($id)
{
$model = $this->findModel($id);
if ($model->load(Yii::$app->request->post())) {
$imageName = $model->marketing_image_name;
$oldImage = MarketingImage::find('marketing_image_name')
->where(['id' => $id])
->one();
if ($oldImage->marketing_image_name != $imageName){
throw new ForbiddenHttpException
('You cannot change the name, you must delete instead.');
}
if( $model->file = UploadedFile::getInstance($model, 'file')){
$thumbName = 'uploads/' . 'thumbnail/' . $imageName . 'thumb.'
. $model->file->extension;
$model->save();
} else {
$model->save();
540
}
if ($model->file) {
541
unlink($model->marketing_thumb_path);
$model->delete();
return $this->redirect(['index']);;
}
catch(\Exception $e) {
throw new NotFoundHttpException($e->getMessage());
}
}
/**
* Finds the MarketingImage model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be thrown.
* @param string $id
* @return MarketingImage the loaded model
* @throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($id)
{
if (($model = MarketingImage::findOne($id)) !== null) {
return $model;
} else {
throw new NotFoundHttpException
('The requested page does not exist.');
}
}
}
So lets just discuss whats new. We start with a new use statement:
use yii\imagine\Image;
542
543
We will also have to account for the marketing_thumb_path attribute and for the actual thumbnail
images themselves.
So after we set $fileName, we will assign a local variable to $thumbName and then do the same kind
of formatting to make sure we dont have spaces in the filename:
$thumbName = 'uploads/' . 'thumbnail/' . $imageName . 'thumb.'
. $model->file->extension;
$thumbName = preg_replace('/\s+/', '', $thumbName);
Besides stripping out the spaces, if there are any, we are also appending thumb onto $imageName
for the $thumbName. If we didnt do that, your thumbnail images would have the same filename as
your primary image. Im not a fan of that. So even though they are in separate folders, I like to give
them different names.
$thumbName will result in a path that we can use to set the $model->marketing_thumb_path
attribute. We have to set that one manually because we are not doing it in the form.
The reason for this is that the thumbnails are auto-generated from our primary image, so we dont
want the user to be able to set the value. Its too easy for them to make a typo.
Once weve set that attribute, we can do a $model->save():
$model->marketing_image_path = $fileName;
$model->marketing_thumb_path = $thumbName;
$model->save();
That takes care of the DB, but we still need to save our primary image and create a thumbnail from
it. Fortunately, our imagine Image class makes this a snap:
544
$model->file->saveAs($fileName);
Image::thumbnail( $fileName , 60, 60)
->save($thumbName, ['quality' => 50]);
In this case, $fileName is the file we are creating the thumbnail from. 60, 60 are dimensions,
$thumbName is the path to save to, and quality is set to 50. Its really just one line of code, very
simple indeed.
And the rest is the same as we had before. You can test this and it should work perfectly, however
we have not added the thumbnail in a view anywhere yet, so you wont see it. You can check it by
checking in the thumbnail folder.
Next we check to see if there is a new file uploaded, and if so, set the $thumbName, which we will
use to create the new thumb, and $model_save():
545
If we dont have file uploaded, which is now possible because we used a scenario to only enforce
the require rule on create, then we simply save the model:
else {
$model->save();
}
Next, if we do have a file uploaded, we need to set the $fileName, then save the file and create the
thumbnail:
if ($model->file) {
So we are only saving a primary image and thumbnail image if there is an image uploaded, otherwise
we are happily redirected to the view with the $model->id that we handed in, same as we had before
546
catch(\Exception $e) {
throw new NotFoundHttpException($e->getMessage());
}
}
Nothing too difficult there. We obviously have to unlink both files to get rid of the image completely.
Notice the \ in the catch:
catch(\Exception $e) {
When you are using built-in PHP classes such as Exception, you have to put a \ in front or Yii 2 will
not find it.
The other thing I did was throw an exception:
throw new NotFoundHttpException($e->getMessage());
So what that does is if one of the files is missing, or it cant complete the unlink, it will return
the message formatted gracefully, which means it will still have the page layout instead of a raw
message on a white background.
Modify Views
Modify View
Were just adding the thumbnail, but Ill give you the entire file for reference.
Gist:
view.php
From book:
<?php
use yii\helpers\Html;
use yii\widgets\DetailView;
/* @var $this yii\web\View */
/* @var $model backend\models\MarketingImage */
$this->title = $model->id;
$this->params['breadcrumbs'][] =
['label' => 'Marketing Images', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="marketing-image-view">
<p>
<?= Html::a('Update', ['update', 'id' => $model->id],
['class' => 'btn btn-primary']) ?>
<?= Html::a('Delete', ['delete', 'id' => $model->id], [
'class' => 'btn btn-danger',
'data' => [
'confirm' => 'Are you sure you want to delete this item?',
'method' => 'post',
],
]) ?>
</p>
<h1><?= Html::encode($model->marketing_image_name) ?></h1>
547
<br>
<div>
<?php
echo Html::img('/'. $model->marketing_image_path . '?'. 'time='
. time() , ['width' => '600px']);
?>
</div>
<br>
<div>
<?php
echo Html::img('/'. $model->marketing_thumb_path . '?'. 'time='. time());
?>
</div>
<br>
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'marketing_image_caption_title',
'marketing_image_caption',
'marketing_image_path',
'marketing_thumb_path',
'marketing_image_weight',
//'marketing_image_name',
['attribute' => 'marketing_image_is_featured',
'format' => 'boolean'],
['attribute' => 'marketing_image_is_active',
'format' => 'boolean'],
'status.status_name',
'created_at',
'updated_at',
],
]) ?>
</div>
548
Modify Update
Again just adding the thumb and providing the entire file for reference.
Gist:
update.php
From book:
<?php
use yii\helpers\Html;
/* @var $this yii\web\View */
/* @var $model backend\models\MarketingImage */
$this->title = 'Update Marketing Image: ' . ' ' . $model->id;
$this->params['breadcrumbs'][] =
['label' => 'Marketing Images', 'url' => ['index']];
$this->params['breadcrumbs'][] =
['label' => $model->id, 'url' => ['view', 'id' => $model->id]];
$this->params['breadcrumbs'][] = 'Update';
?>
<div class="marketing-image-update">
<h1><?= Html::encode($this->title) ?></h1>
<br>
<div>
<?php
echo Html::img('/'. $model->marketing_image_path, ['width' => '600px']);
?>
</div>
<br>
<div>
<?php
echo Html::img('/'. $model->marketing_thumb_path . '?'. 'time='. time());
549
550
?>
</div>
<br>
<?= $this->render('_form', [
'model' => $model,
]) ?>
</div>
Modify _form
Just add the one line:
<?= $form->field($model, 'marketing_image_caption_title') ?>
Modify _search
Again, we just modify one line:
<?= $form->field($model, 'marketing_image_caption_title') ?>
Modify Index
We only have a small change here, but again Im providing the entire file for reference. Doing this
is a way for me to make sure I havent missed something, since this code comes directly from my
IDE.
Gist:
index.php
From book:
551
<?php
use yii\helpers\Html;
use yii\grid\GridView;
use \yii\bootstrap\Collapse;
/* @var $this yii\web\View */
/* @var $searchModel backend\models\search\MarketingImageSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */
$this->title = 'Marketing Images';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="marketing-image-index">
<h1><?= Html::encode($this->title) ?></h1>
<?php
echo Collapse::widget([
'encodeLabels'
'items' => [
=> false,
Search',
?>
<p>
<?= Html::a('Create Marketing Image', ['create'],
552
What a powerful little line thumb:html is. When we call thumb, we are calling the model method,
using magic get syntax, so that is actually calling getThumb from the model. The part after the colon
tells gridview its returning html. And thats it! You get a clickable thumbnail in your grid. You also
get the column heading.
There are a lot of ways to appreciate the power of Yii 2 and this is certainly one of them.
Ok, so now that we got our image management up and running, we want to put our images to good
use in the frontend.
553
URL Manager
Because we are using Yii 2s advanced template, using the images from the backend in the frontend
takes some configuration. This is because the frontend and backend are two separate applications.
So what we need to do is add to communicate with the backend path, and we do that by setting up
a key in our components array. I should note that I got this solution from Skworden in the forum
after I spent quite a few hours being stumped by it. Yii 2 has a great community of developers and
that can really help you out when you need it.
Anyway, what we need to do is add the following to your frontend/config/main.php file in your
components array:
//can name it whatever you want as it is custom
'urlManagerBackend' => [
'class' => 'yii\web\urlManager',
'baseUrl' => 'http://backend.yii2build.com',
'enablePrettyUrl' => true,
'showScriptName' => false,
],
I should also note that this is still a bit of a workaround. I would prefer not to use an absolute url
here if I dont have to because its not as efficient as a relative link.
If I cant find any other solution, I will eventually reverse it so that the images and thumbnails are
saved to the frontend and referenced by absolute link on the backend. This would be more efficient
in terms of server resource.
Every once in a while you come across something you cant instantly solve, and this is especially
true with a big framework like Yii 2. It takes a long time to learn every nuance of the framework, so
everyone, including me, has to have patience for that.
Anyway, once you have that in place, you can reference images that are stored in the backend, like
so:
echo Html::img(Yii::$app->urlManagerBackend->baseUrl.'/uploads/your-image.png');
Im going to give you a Gist for reference on the entire file, in case you need to debug. Remember,
the path is frontend/config/main.php:
Gist:
main.php
554
Carousel Widget
Ok, our objective will be to have a carousel widget that we can embed on the index page of the site,
which just as a reminder, is controlled by the pages controller.
The idea will be to be able to call our carousel with a single line of code, like so:
<?= CarouselWidget::widget(['settings' => [
'height' => '300px',
'width' => '700px']])?>
You can see that we will have the option to hand in settings, in this case it will be height and width
of the images. We will also build our widget to have default settings, so we could call it like so:
<?= CarouselWidget::widget()?>
We will create the widget so that it sets default values if we choose to call it without settings.
When we created our MarketingImages model, we made three attributes that will be used to
determine if the image will be shown in the carousel. One is the marketing_image_is featured
boolean column, which if true, means the image is displayed.
The second is the marketing_image_is_active boolean column, which if true, means the image is the
active image, which means it is the one that is first loaded in the carousel. So when you are creating
images, you should only have one image set to true.
The last column we test for is the status column, and we only want the images that are active.
So this data structure is nice and flexible, while giving the admin total control via UI to display or
not to display images in the carousel.
Before we code the actual widget, we need to setup the files.
If you remember from chapter 12, when we introduced you to widgets, the basic structure is two
files, a widget file directly under the components directory and a view file, which is in the views
folder inside the components directory.
So you can see the two files we added there. I will give you the files in a moment. First, lets set up
our component. Go to common\config\main.php and add this to your components array:
'carouselwidget' => [
'class' => 'components\CarouselWidget',
],
CarouselWidget.php
Gist:
CarouselWidget.php
from book:
<?php
namespace components;
use
use
use
use
use
use
use
yii\base\Widget;
yii\helpers\Html;
Yii;
backend\models\MarketingImage;
backend\models\search\MarketingImageSearch;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
$activeImage;
$images;
$count;
$settings = [];
555
$this->activeImage = MarketingImage::find('marketing_image_path')
->where(['marketing_image_is_active' => 1])
->andWhere(['marketing_image_is_featured' => 1])
->andWhere(['status_id' => 1])
->one();
$this->images = MarketingImage::find()
->where(['marketing_image_is_active' => 0])
->andWhere(['marketing_image_is_featured' => 1])
->andWhere(['status_id' => 1])
->orderBy('marketing_image_weight')
->all();
$this->count = MarketingImage::find()
->where(['marketing_image_is_active' => 0])
->andWhere(['marketing_image_is_featured' => 1])
->andWhere(['status_id' => 1])
->count();
$this->setDefaults();
$this->validateSize();
}
public function setDefaults()
{
if(!isset($this->settings['height'])){
$this->settings['height'] = '300px';
}
if(!isset($this->settings['width'])){
$this->settings['width'] = '700px';
}
if(!isset($this->settings['autoplay'])){
$this->settings['autoplay'] = true;
556
}
$width = (int) preg_replace("/[^0-9]/","",$this->settings['width']);
switch ($width){
case $width < 40 :
throw new NotFoundHttpException('You Must Stay within
557
558
}
}
public function run()
{
return $this->render('carousel',
['activeImage' => $this->activeImage,
'images' => $this->images,
'count' => $this->count,
'settings' => $this->settings]);
}
Since we covered the creation of a custom widget in chapter 12, Im going to step through this
quickly. Obviously, we include everything we need in the use statements, then move on to declaring
the class properties:
public
public
public
public
$activeImage;
$images;
$count;
$settings = [];
The $activeImage property will hold the ActiveRecord instance with the image that has marketing_image_is_active set to a value of 1. Please remember to keep only one record with that value.
The $images property will be an instance of ActiveRecord with multiple results, all the images that
are set to featured.
Lastly, the $settings array will hold our settings values when they are handed in.
So moving on to init():
559
$this->setDefaultSize();
$this->validateSize();
}
You can see we call the parent::init() and then we use ActiveRecord to return the results we are
looking for and set them to the class properties via $this.
Note in the $this->images instance we are ordering by marketing_image_weight. This is really
simple stuff and it gives us complete control over the order that the images will be displayed in
the carousel.
We use the $count property in the view to determine the correct number of carousel-indicators to
display.
Next we call a couple of methods I created to set the default size of the images if none is provided
and also to do just a little validation on the size input. Lets look at the setDefaults first:
560
Next I decided to do a little validation on the size input, so that we can have a hint if something in
the settings has a typo:
public function validateSize()
{
if (!preg_match("/px/", $this->settings['width'])
or !preg_match("/px/", $this->settings['height'])) {
throw new NotFoundHttpException
('You Must Use px and number for size, example 300px');
}
$height = (int) preg_replace("/[^0-9]/","",$this->settings['height']);
switch ($height){
case $height < 40 :
throw new NotFoundHttpException('You Must Stay within
40 to 1000 px and use px and number for size, example 300px');
break;
case $height > 1000 :
throw new NotFoundHttpException('You Must Stay within
40 to 1000 px and use px and number for size, example 300px');
break;
}
$width = (int) preg_replace("/[^0-9]/","",$this->settings['width']);
switch ($width){
case $width < 40 :
throw new NotFoundHttpException('You Must Stay within
40 to 1000 px and use px and number for size, example 300px');
break;
case $width > 1000 :
throw new NotFoundHttpException('You Must Stay within
40 to 1000 px and use px and number for size, example 300px');
break;
}
}
561
562
if (!preg_match("/px/", $this->settings['width'])
or !preg_match("/px/", $this->settings['height'])) {
throw new NotFoundHttpException
('You Must Use px and number for size, example 300px');
}
Then I set minimum and maximum values for size. But first I had to extract the number from the
string:
$height = (int) preg_replace("/[^0-9]/","",$this->settings['height']);
I probably could have written a stronger set of validation rules, but I felt it would have been overkill,
since were just looking for a hint of what could be wrong if the carousel does not display properly.
Moving on, we get to the run method:
We just render carousel.php and pass in the properties we will need in the view.
carousel.php
Now we are ready to look at the widget view:
Gist:
carousel.php
From book:
<?php
use yii\helpers\Html;
?>
<div id="carouselMain" class="carousel slide"
<?php
if($settings['autoplay'] == false ){
echo 'data-interval="false"';
}
?>
data-ride="carousel">
<!-- Indicators -->
<ol class="carousel-indicators">
563
564
565
</div>';
//all other images
foreach ($images as $image){
echo '<div class="item">
<center>' .
Html::img(Yii::$app->urlManagerBackend->baseUrl . '/' .
$image['marketing_image_path'],
['width' => $width, 'height' => $height ])
. '</center>
<div class="carousel-caption">'.$image['marketing_image_caption'].'
</div>
</div>';
}
?>
<!-- end dynamic slide data -->
</div>
<!-- Controls -->
<a class="left carousel-control" href="#carouselMain" role="button"
data-slide="prev">
<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="right carousel-control" href="#carouselMain" role="button"
data-slide="next">
<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
Looks like a giant plate of spaghetti, but this just a straight copy and paste from Bootstrap 3, with a
few dynamic elements.
When working with PHP and HTML together, things get ugly quickly. Sometimes I think its actually
easier to work with this on your own than to read someone elses code.
566
You have to very careful to use single quotes and place them very precisely. Dont worry, we will
step through this.
So you can see what changes we made, here is the original bootstrap code for reference:
Bootstrap 3 Carousel
We named our element carouselMain instead of carousel-example-generic:
<div id="carouselMain" class="carousel slide"
Then we get an if statement to see if we want the carousel to autoplay, which we can set in our
settings, when we call the widget. It defaults to true, but if autoplay is set to false, we echo the
following:
<?php
if($settings['autoplay'] == false ){
echo 'data-interval="false"';
}
?>
?>
Next we have the section that echoes the active item and the other items:
567
To make the code easier to read, Im assigning the value of $width and $height, but you could easily
just pop $settings[height] into Html image tag.
568
In the case of the items, we use a foreach loop to carefully echo out the exact syntax need for the
carousel, while injecting our dynamic data.
For the image tag, we are using:
Html::img(Yii::$app->urlManagerBackend->baseUrl . '/'
. $image['marketing_image_path'], ['width' => $width, 'height' => $height ])
You can see we are using our urlManagerBackend to get the baseurl that allows us to reference an
image from the backend.
Pages Index
Im giving you the entire file for reference, but its only the top area that has changed. You can see
where we put the widget. I set the autoplay to false:
<div class="jumbotron">
<br>
<div>
<?= CarouselWidget::widget(['settings' =>
['height' => '300px', 'width' => '700px'
'autoplay'=>false]])?>
</div>
</div>
Gist:
Pages Index
From book:
<?php
use \yii\bootstrap\Modal;
use kartik\social\FacebookPlugin;
use \yii\bootstrap\Collapse;
use \yii\bootstrap\Alert;
use yii\helpers\Html;
use components\FaqWidget;
use components\CarouselWidget;
/* @var $this yii\web\View */
$this->title = 'My Yii Application';
?>
<div class="site-index">
<?php
if (Yii::$app->user->isGuest) {
echo yii\authclient\widgets\AuthChoice::widget([
'baseAuthUrl' => ['site/auth'],
'popupMode' => false,
]);
}
?>
<div class="jumbotron">
<br>
<div>
<?= CarouselWidget::widget(['settings' =>
['height' => '300px', 'width' => '700px'
'autoplay'=>false]])?>
</div>
</div>
<?php
echo Collapse::widget([
569
'items' => [
[
'label' => 'Top Features' ,
'content' => FacebookPlugin::widget([
'type'=>FacebookPlugin::SHARE,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]),
// open its content by default
//'contentOptions' => ['class' => 'in']
],
// another group item
[
'label' => 'Top Resources',
'content' => FacebookPlugin::widget([
'type'=>FacebookPlugin::SHARE,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]),
// 'contentOptions' => [],
// 'options' => [],
],
]
]);
Modal::begin([
570
FacebookPlugin::widget([
'type'=>FacebookPlugin::COMMENT,
'settings' => ['href'=>'http://www.yii2build.com','width'=>'350']
]);
Modal::end();
?>
<br/>
<br/>
<?Php
echo Alert::widget([
'options' => [
'class' => 'alert-info',
],
'body' => 'Launch your project like a rocket...',
]);
?>
<div class="body-content">
<div class="row">
<div class="col-lg-4">
<h2>Free</h2>
<p>
<?php
if (!Yii::$app->user->isGuest) {
571
572
573
574
</div>
Carousel Settings
Once I had all that done, I thought, wouldnt it be cool if you could hand all control of the carousel
to admin via the backend? Clients would love that. That shouldnt be too hard if you think about it,
we have everything we need in place, except for the settings model.
This seemed like fun, so I took it up as a challenge to see how many of the carousel settings I could
manage from backend UI and Im fairly satisfied with the result. It turns out there are more settings
that we have previously played with.
Lets start by setting up a CarouselSettings model. This is where we will store all of our carousel
settings. And we will have access to them via the backend, just like we can access roles and statuses,
etc.
carousel_settings table
Here is a screenshot of what we need:
SQL:
575
Note that in the above, I created friendlier settings names when I could use a single word, like height,
for example. For multiple words, I kept it exactly as it is in the DB.
CarouselSettings Model
Lets start by using Gii to create the model.
Gist:
CarouselSettings Model
From book:
<?php
namespace backend\models;
use
use
use
use
use
use
Yii;
yii\db\ActiveRecord;
yii\db\Expression;
yii\helpers\ArrayHelper;
yii\behaviors\TimestampBehavior;
yii\helpers\Html;
/**
* This is the model class for table "carousel_settings".
*
* @property integer $id
* @property string $carousel_name
* @property string $image_height
* @property string $image_width
* @property integer $carousel_autoplay
* @property integer $show_indicators
* @property integer $status_id
* @property string $created_at
* @property string $updated_at
*
* @property Status $status
*/
class CarouselSettings extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
576
{
return 'carousel_settings';
}
public function behaviors()
{
return [
'timestamp' => [
'class' => TimeStampBehavior::className(),
'attributes' => [
ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
],
'value' => new Expression('NOW()'),
],
];
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['carousel_name', 'image_height',
'image_width', 'caption_font_size', 'status_id'], 'required'],
[['carousel_autoplay', 'show_indicators', 'show_captions',
'status_id', 'show_controls'], 'integer'],
[['carousel_autoplay'],'in',
'range'=>array_keys($this->getCarouselAutoplayList())],
[['show_indicators'],'in',
'range'=>array_keys($this->getShowIndicatorsList())],
[['show_captions'],'in',
'range'=>array_keys($this->getShowCaptionsList())],
[['show_caption_background'],'in',
'range'=>array_keys($this->getShowCaptionBackgroundList())],
[['show_caption_title'],'in',
'range'=>array_keys($this->getShowCaptionTitleList())],
[['show_controls'],'in',
'range'=>array_keys($this->getShowControlsList())],
577
[['status_id'],'in', 'range'=>array_keys($this->getStatusList())],
[['created_at', 'updated_at'], 'safe'],
[['carousel_name', 'image_height', 'image_width'],
'string', 'max' => 45]
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'carousel_name' => 'Name',
'image_height' => 'Height',
'image_width' => 'Width',
'carousel_autoplay' => 'Autoplay',
'show_indicators' => 'Show Indicators',
'show_captions' => 'Show Captions',
'show_caption_background' => 'Show Caption Background',
'show_caption_title' => 'Show Caption Title',
'show_controls' => 'Show Controls',
'caption_font_size' => 'Caption Size',
'status_id' => 'Status',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'statusName' => Yii::t('app', 'Status'),
];
}
/**
* @return \yii\db\ActiveQuery
*/
public function getStatus()
{
return $this->hasOne(Status::className(),
['id' => 'status_id']);
578
}
public function getStatusName()
{
return $this->status ? $this->status->status_name : '- no status -';
}
/**
* get list of statuses for dropdown
*/
public static function getStatusList()
{
$droptions = Status::find()->asArray()->all();
return ArrayHelper::map($droptions, 'id', 'status_name');
}
public static function getCarouselAutoplayList()
{
return $droptions = [0 => "no", 1 => "yes"];
}
public static function getShowIndicatorsList()
{
return $droptions = [0 => "no", 1 => "yes"];
}
public static function getShowCaptionsList()
{
return $droptions = [0 => "no", 1 => "yes"];
}
public static function getShowCaptionTitleList()
{
return $droptions = [0 => "no", 1 => "yes"];
}
public static function getShowCaptionBackgroundList()
{
return $droptions = [0 => "no", 1 => "yes"];
}
579
580
There really isnt anything on the model that we havent seen before. I did shorten some of the
attribute labels so we could get all the columns without scrolling.
CarouselSettingsSearch Model
Gist:
CarouselSettingsSearch Model
From book:
<?php
namespace backend\models\search;
use
use
use
use
Yii;
yii\base\Model;
yii\data\ActiveDataProvider;
backend\models\CarouselSettings;
/**
* CarouselSettingsSearch represents the model
* behind the search form about `backend\models\CarouselSettings`.
*/
class CarouselSettingsSearch extends CarouselSettings
{
public $statusName;
/**
* @inheritdoc
*/
public function rules()
{
return [
[['id', 'carousel_autoplay', 'show_indicators',
'show_captions', 'show_controls',
'show_caption_background','show_caption_title',
'status_id'], 'integer'],
[['carousel_name', 'image_height', 'image_width',
'caption_font_size', 'statusName', 'created_at',
'updated_at'], 'safe'],
];
}
/**
* @inheritdoc
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = CarouselSettings::find();
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$dataProvider->setSort([
'attributes' => [
'id',
'carousel_name',
'image_height',
'image_width',
'carousel_autoplay',
'show_indicators',
581
582
'show_captions',
'show_caption_title',
'show_caption_background',
'caption_font_size',
'show_controls',
'statusName' => [
'asc' => ['status.status_name' => SORT_ASC],
'desc' => ['status.status_name' => SORT_DESC],
'label' => 'Status'
],
'updated_at',
]
]);
if (!($this->load($params) && $this->validate())) {
$query->joinWith(['status']);
return $dataProvider;
}
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
$this->addSearchParameter($query,
'id');
'carousel_name', true);
'image_height');
'image_width');
'carousel_autoplay');
'show_indicators');
'show_captions');
'show_caption_title');
'show_caption_background');
'caption_font_size');
'show_controls');
'status_id');
}]);
return $dataProvider;
}
protected function addSearchParameter($query, $attribute, $partialMatch = false)
{
if (($pos = strrpos($attribute, '.')) !== false) {
$modelAttribute = substr($attribute, $pos + 1);
} else {
$modelAttribute = $attribute;
}
$value = $this->$modelAttribute;
if (trim($value) === '') {
return;
}
/*
* The following line is additionally added for right aliasing
* of columns so filtering happen correctly in the self join
*/
$attribute = "carousel_settings.$attribute";
if ($partialMatch) {
$query->andWhere(['like', $attribute, $value]);
} else {
$query->andWhere([$attribute => $value]);
}
}
583
584
}
}
Most of this is boilerplate, but we do have the getCarouselSettings method to return an AR instance
holding our settings. Youll note that we pass $carousel_name into the method signature and also
that its static, so we could call it like so:
$carouselSettings = CarouselSettingsSearch::getCarouselSettings('Front Page');
We could then pass $carouselSettings into a view via a controller and have access to all the settings.
CarouselSettingsController
Gist:
CarouselSettings Controller
From book:
<?php
namespace backend\controllers;
use
use
use
use
use
use
use
Yii;
backend\models\CarouselSettings;
backend\models\search\CarouselSettingsSearch;
yii\web\Controller;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
common\models\PermissionHelpers;
/**
* CarouselSettingsController implements the CRUD actions for CarouselSettings m\
odel.
*/
class CarouselSettingsController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => \yii\filters\AccessControl::className(),
'only' => ['index', 'view','create', 'update', 'delete'],
'rules' => [
[
'actions' => ['index', 'view', 'create', 'update', 'delete'],
'allow' => true,
'roles' => ['@'],
'matchCallback' => function ($rule, $action) {
return PermissionHelpers::requireMinimumRole('Admin')
&& PermissionHelpers::requireStatus('Active');
}
],
],
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
];
}
/**
* Lists all CarouselSettings models.
* @return mixed
*/
public function actionIndex()
{
$searchModel = new CarouselSettingsSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
585
}
/**
* Displays a single CarouselSettings model.
* @param integer $id
* @return mixed
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}
/**
* Creates a new CarouselSettings model.
* If creation is successful, the browser will be
* redirected to the 'view' page.
* @return mixed
*/
public function actionCreate()
{
$model = new CarouselSettings();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
} else {
return $this->render('create', [
'model' => $model,
]);
}
}
/**
* Updates an existing CarouselSettings model.
* If update is successful, the browser will be
* redirected to the 'view' page.
* @param integer $id
* @return mixed
*/
public function actionUpdate($id)
586
{
$model = $this->findModel($id);
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
} else {
return $this->render('update', [
'model' => $model,
]);
}
}
/**
* Deletes an existing CarouselSettings model.
* If deletion is successful, the browser will be
* redirected to the 'index' page.
* @param integer $id
* @return mixed
*/
public function actionDelete($id)
{
$this->findModel($id)->delete();
return $this->redirect(['index']);
}
/**
* Finds the CarouselSettings model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be thrown.
* @param integer $id
* @return CarouselSettings the loaded model
* @throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($id)
{
if (($model = CarouselSettings::findOne($id)) !== null) {
return $model;
} else {
throw new NotFoundHttpException('The requested page does not exist.');
}
}
}
587
All were doing there is checking for admin permission, the rest is straight from Gii.
588
->dropDownList($model->showCaptionsList,
['prompt' => 'Please Choose One']);?>
<?= $form->field($model, 'show_caption_background')
->dropDownList($model->showCaptionBackgroundList,
['prompt' => 'Please Choose One']);?>
<?= $form->field($model, 'caption_font_size')
->textInput(['maxlength' => 45]) ?>
<?= $form->field($model, 'show_controls')
->dropDownList($model->showControlsList,
['prompt' => 'Please Choose One']);?>
<?= $form->field($model, 'status_id')
->dropDownList($model->statusList);?>
<div class="form-group">
<?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update',
['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
We just cleaned up the form with dropdown lists, like we always do.
589
<?php
use yii\helpers\Html;
use yii\widgets\DetailView;
/* @var $this yii\web\View */
/* @var $model backend\models\CarouselSettings */
$this->title = $model->carousel_name;
$this->params['breadcrumbs'][] = ['label' => 'Carousel Settings',
'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title . ' Carousel';
?>
<div class="carousel-settings-view">
<h1><?= Html::encode($model->carousel_name) ?> Carousel</h1>
<p>
<?= Html::a('Update', ['update', 'id' => $model->id],
['class' => 'btn btn-primary']) ?>
<?= Html::a('Delete', ['delete', 'id' => $model->id], [
'class' => 'btn btn-danger',
'data' => [
'confirm' => 'Are you sure you want to delete this item?',
'method' => 'post',
],
]) ?>
</p>
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'carousel_name',
'image_height',
'image_width',
['attribute' => 'carousel_autoplay', 'format' => 'boolean'],
['attribute' => 'show_indicators', 'format' => 'boolean'],
['attribute' => 'show_caption_title', 'format' => 'boolean'],
['attribute' => 'show_captions', 'format' => 'boolean'],
['attribute' => 'show_caption_background', 'format' => 'boolean'],
'caption_font_size',
590
591
Again, this should be familiar to us at this point. Im providing it for reference, but you shouldnt
need it.
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'id',
'carousel_name',
'image_height',
'image_width',
['attribute' => 'carousel_autoplay', 'format' => 'boolean'],
['attribute' => 'show_indicators', 'format' => 'boolean'],
['attribute' => 'show_caption_title', 'format' => 'boolean'],
['attribute' => 'show_captions', 'format' => 'boolean'],
['attribute' => 'show_caption_background', 'format' => 'boolean'],
'caption_font_size',
['attribute' => 'show_controls', 'format' => 'boolean'],
'statusName',
//'created_at',
'updated_at',
['class' => 'yii\grid\ActionColumn'],
],
]); ?>
</div>
This has the same minor formatting changes that we always have on index.php.
592
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/* @var $this yii\web\View */
/* @var $model backend\models\search\CarouselSettingsSearch */
/* @var $form yii\widgets\ActiveForm */
?>
<div class="carousel-settings-search">
<?php $form = ActiveForm::begin([
'action' => ['index'],
'method' => 'get',
]); ?>
<?= $form->field($model, 'id') ?>
<?= $form->field($model, 'carousel_name')->textInput(['maxlength' => 45]) ?>
<?= $form->field($model, 'image_height')->textInput(['maxlength' => 45]) ?>
<?= $form->field($model, 'image_width')->textInput(['maxlength' => 45]) ?>
<?= $form->field($model, 'carousel_autoplay')
->dropDownList($model->carouselAutoplayList,
['prompt' => 'Please Choose One']);?>
<?= $form->field($model, 'show_indicators')
->dropDownList($model->showIndicatorsList,
['prompt' => 'Please Choose One']);?>
<?= $form->field($model, 'show_caption_title')
->dropDownList($model->showCaptionTitleList,
['prompt' => 'Please Choose One']);?>
<?= $form->field($model, 'show_captions')
->dropDownList($model->showCaptionsList,
['prompt' => 'Please Choose One']);?>
<?= $form->field($model, 'show_caption_background')
593
594
->dropDownList($model->showCaptionBackgroundList,
['prompt' => 'Please Choose One']);?>
<?= $form->field($model, 'caption_font_size')
->textInput(['maxlength' => 45]) ?>
<?= $form->field($model, 'show_controls')
->dropDownList($model->showControlsList,
['prompt' => 'Please Choose One']);?>
<?= $form->field($model, 'status_id')->dropDownList($model->statusList);?>
<?php echo $form->field($model, 'updated_at') ?>
<div class="form-group">
<?= Html::submitButton('Search', ['class' => 'btn btn-primary']) ?>
<?= Html::resetButton('Reset', ['class' => 'btn btn-default']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
Were not using search, especially since we will only have one record, but I formatted it anyway, in
case things change in the future and we find ourselves searching for carousels.
PagesController
Gist:
Pages Controller
From book:
<?php
namespace frontend\controllers;
use
use
use
use
Yii;
frontend\models\ContactForm;
yii\filters\AccessControl;
backend\models\search\CarouselSettingsSearch;
595
];
}
public function actionIndex()
{
$carouselSettings = CarouselSettingsSearch::getCarouselSettings('Front Page');
return $this->render('index', ['carouselSettings' => $carouselSettings]);
}
596
597
return $this->render('privacy');
}
public function actionTermsService()
{
return $this->render('terms-service');
}
}
Here we are calling our getCarouselSettings method, so we can pass it to Pages index view.
CarouselWidget
Gist:
CarouselWidget
From book:
<?php
namespace components;
use
use
use
use
use
use
use
yii\base\Widget;
yii\helpers\Html;
Yii;
backend\models\MarketingImage;
backend\models\search\MarketingImageSearch;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
$activeImage;
$images;
$count;
$settings = [];
598
->orderBy('marketing_image_weight')
->all();
$this->count = MarketingImage::find()
->where(['marketing_image_is_active' => 0])
->andWhere(['marketing_image_is_featured' => 1])
->andWhere(['status_id' => 1])
->count();
$this->setDefaults();
$this->validateSize();
}
public function setDefaults()
{
if(!isset($this->settings['height'])){
$this->settings['height'] = '300px';
}
if(!isset($this->settings['width'])){
$this->settings['width'] = '700px';
}
if(!isset($this->settings['caption_font_size'])){
$this->settings['caption_font_size'] = '15px';
}
if(!isset($this->settings['autoplay'])){
$this->settings['autoplay'] = true;
}
if(!isset($this->settings['show_indicators'])){
$this->settings['show_indicators'] = true;
}
599
if(!isset($this->settings['show_captions'])){
$this->settings['show_captions'] = false;
}
if(!isset($this->settings['show_caption_background'])){
$this->settings['show_caption_background'] = false;
}
if(!isset($this->settings['show_caption_title'])){
$this->settings['show_caption_title'] = false;
}
if(!isset($this->settings['show_controls'])){
$this->settings['show_controls'] = true;
}
}
public function validateSize()
{
if (!preg_match("/px/", $this->settings['width'])
or !preg_match("/px/", $this->settings['height'])) {
throw new NotFoundHttpException
('You Must Use px and number for size, example 300px');
}
$height = (int) preg_replace("/[^0-9]/","",$this->settings['height']);
switch ($height){
case $height < 40 :
600
601
}
$width = (int) preg_replace("/[^0-9]/","",$this->settings['width']);
switch ($width){
case $width < 40 :
throw new NotFoundHttpException('You Must Stay within
40 to 1000 px and use px and number for size, example 300px');
break;
case $width > 1000 :
throw new NotFoundHttpException('You Must Stay within
40 to 1000 px and use px and number for size, example 300px');
break;
}
}
public function run()
{
return $this->render('carousel', ['activeImage' => $this->activeImage,
'images' => $this->images,
'count' => $this->count,
'settings' => $this->settings]);
}
}
Obviously we have more defaults in our setDefaults method. Also, a reminder that some of the
602
settings are shortened, like in the case of height and width. Their names are different in the model
and DB, but you can call them what you want in the widget, as long as you remember what you
named them.
carousel.php
That brings us to carousel.php.
Gist:
carousel.php
From book:
<?php
use yii\helpers\Html;
?>
<?php
if ($settings['show_indicators']){
echo '<ol class="carousel-indicators">
<li data-target="#carouselMain" data-slide-to="0"
class="active"></li>';
foreach (range(1, $count) as $number) {
603
604
'</h1></div>';
}
echo $activeImage['marketing_image_caption'] . '</div>';
}
echo '</div>';
//all other images
foreach ($images as $image){
echo '<div class="item">
<center>' .
Html::img(Yii::$app->urlManagerBackend->baseUrl .
'/' . $image['marketing_image_path'],
['width' => $width, 'height' => $height ])
. '</center>';
if($settings['show_captions']){
echo '<div class="carousel-caption">';
if ($settings['show_caption_title']){
echo
'<div><h1>' .
$image['marketing_image_caption_title'].
'</h1></div>';
}
echo $image['marketing_image_caption'].' </div>';
}
echo '</div>';
}
?>
605
}
?>
</div>
The really simple explanation to that code is we simply added a bunch of if statements that rely on
our settings and dynamic data. For example:
if($settings['show_captions']){
So if show_captions is true, we get to see the captions. Then nested further down, we test for the
caption title:
606
if ($settings['show_caption_title']){
autoplay
show_indicators
show_captions
show_caption_title
show_controls
Main
Ill give you the entire file first, then well focus on the relevant bits.
Gist:
layouts/main.php
Froom book:
<?php
use yii\helpers\Html;
use yii\bootstrap\Nav;
use yii\bootstrap\NavBar;
use yii\widgets\Breadcrumbs;
use frontend\assets\AppAsset;
use frontend\widgets\Alert;
use frontend\assets\FontAwesomeAsset;
use backend\models\search\CarouselSettingsSearch;
NavBar::begin([
607
if (Yii::$app->user->isGuest) {
$menuItems[] = ['label' => 'Signup', 'url' => ['/site/signup']];
$menuItems[] = ['label' => 'Login', 'url' => ['/site/login']];
} else {
$menuItems[] = ['label' => 'Social Sync', 'items' => [
['label' => '<i class="fa fa-facebook"></i> Facebook',
'url' => ['site/auth', 'authclient' => 'facebook']],
['label' => '<i class="fa fa-github"></i> Github',
'url' => ['site/auth', 'authclient' => 'github']],
['label' => '<i class="fa fa-twitter"></i> Twitter',
'url' => ['site/auth', 'authclient' => 'twitter']],
['label' => '<i class="fa fa-linkedin"></i> LinikedIn',
'url' => ['site/auth', 'authclient' => 'linkedin']],
['label' => '<i class="fa fa-google"></i> Google+',
'url' => ['site/auth', 'authclient' => 'google']],
]];
$menuItems[] = ['label' => 'Profile', 'url' => ['/profile/view']];
$menuItems[] = [
'label' => 'Logout (' . Yii::$app->user->identity->username . ')',
'url' => ['/site/logout'],
608
609
So to take control of the desired CSS elements, we first have to include our use statement:
use backend\models\search\CarouselSettingsSearch;
Next we added a block of PHP that will echo out the style tags with our if statements controlling
the scenario:
610
<?php
$carouselSettings = CarouselSettingsSearch::getCarouselSettings('Front Page');
if($carouselSettings['caption_font_size']){
echo '<style>.carousel-caption{';
if ($carouselSettings['show_caption_background']){
echo 'background: rgba(0,0,0,0.5);';
}
echo 'font-size:' . $carouselSettings['caption_font_size'] . ';
}
</style>';
}
?>
Then if a caption_font_size has been set, we enforce that as well as check for the show_caption_background setting. If there is no caption_font_size selected, there will be no background.
Note that the caption title is controlled by an h1 tag, and you could make a setting for that and
control it in a similar fashion.
And thats pretty much it for our control over the carousel. Play around with the different settings
and have fun.
Summary
This chapter turned out to be extensive. We built our basic image management that lets us upload,
edit and delete images and their corresponding thumbnails.
Along the way we learned about scenarios and we used yii2-imagine extension to make thumbnail
creation as simple as it can be. But since were building a template, we wanted to immediately put
our image management capability to good use. So we decided to build a dynamically-driven carousel
that would display marketing images.
611
Its often very important for our clients to have control over things like their marketing message, so
we also made a full UI for carousel settings that are respected by the custom widget we built. This
gives a lot of control over the carousel directly to the client and they will love that.
While I was writing this chapter, some of our readers made nice comments, along with positive
reviews and ratings on Goodreads.com. That really inspired me to work as hard as I could to deliver
all the features in the carousel, which took multiple revisions on my part. So please keep all the
comments and reviews coming, I really appreciate it.
Thanks once again for taking the Yii 2 journey with me. More will be coming. I hope to see you
soon.
composer update
613
Next we need to decide on a data structure. I decided to call my new table faq_rating:
faq_rating table
Youll note that we chose double as our datatype for faq_rating and this allows us to have the
decimal in the rating such as 2.5 etc.
Next we need to create the model from Gii. Im going to put the model in the backend. Lately Ive
decided to put models in the backend, unless I have good reason to do otherwise.
You are of course free to structure it how you wish. As long as you namespace everything properly,
you have a lot of flexibility in your choices.
You may have noticed that our table references to other primary keys, user_id and faq_id. This is
also known as a junction table or a pivot table. If we needed to reference the relationships, they
would be many to many relationships.
But with a rating system, at least our implementation of it, we dont really need to reference it that
way, so we didnt bother creating foreign keys or setting up the relationships.
If you wanted to get more complex, for example, if you wanted to write a report that showed all
the faq_ratings made by a specific user, then the relationships would be important. But we are not
anticipating that use case for this, so thats just something to keep in mind if you do a more complex
integration with another model.
For our purposes here, we only need to store the ratings so we can calculate an average, store the
faq_id the rating applies to, and store the user_id so that people dont rate more than once.
614
We will also allow people to update their ratings. Alternatively, you could prevent them from doing
so if you wish. I will point out where you could do that later if you want to go in that direction.
So now we have to imagine our implementation. How does the rating fit in with the Faqs?
Ultimately, we want it to look like this:
So if you land on an Faq view page, you see the rating below the faq.
And if you click the Add Your Rating button:
615
So once we click on Add Your Rating, we get the above screen. If you mouse over the stars, they fill
in, then you push the rate it button and the rating gets recorded.
So this seems incredibly easy, until you realize we are combining models, using both the Faq model
and the FaqRating model.
That raises all kinds of interesting questions about what controller is going to do what, as well as
how to mix the models onto a single view page, since the StarRating widget is going to pass us a
value that we need move along via a form.
Taking a clue from Yii 2 itself, it seems like forms are best created as partials, so our plan will be to
create a form partial for our FaqRating model and control form submission through our soon-to-bemade FaqRatingController.
But first, lets look at the model.
FaqRatings Model
Gist:
FaqRatings Model
From book:
<?php
namespace backend\models;
use
use
use
use
use
Yii;
yii\db\ActiveRecord;
yii\db\Expression;
yii\helpers\ArrayHelper;
kartik\widgets\StarRating;
/**
* This is the model class for table "faq_rating".
*
* @property integer $id
* @property integer $user_id
* @property integer $faq_id
* @property double $faq_rating
* @property string $created_at
* @property string $updated_at
*/
class FaqRating extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
{
return 'faq_rating';
}
public function behaviors()
{
return [
'timestamp' => [
'class' => 'yii\behaviors\TimestampBehavior',
'attributes' => [
ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
],
'value' => new Expression('NOW()'),
],
];
616
}
public function beforeValidate()
{
$this->faq_rating = (double)$this->faq_rating;
$this->faq_id = (int)$this->faq_id;
return parent::beforeValidate();
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['user_id', 'faq_id', 'faq_rating',], 'required'],
[['user_id', 'faq_id'], 'integer'],
[['faq_rating'], 'double'],
[['created_at', 'updated_at'], 'safe']
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'user_id' => 'User ID',
'faq_id' => 'Faq ID',
'faq_rating' => 'Faq Rating',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
];
}
public function showAverageRating($faq_id)
{
$averageRating = $this->getAverageRating($faq_id);
617
echo StarRating::widget([
'name' => 'rating_' . $averageRating,
'value' => $averageRating,
'disabled' => true,
'pluginOptions' => [
'size' => 'sm',
'stars' => 5,
'min' => 0,
'max' => 5,
'step' => 0.5,
// 'symbol' => html_entity_decode('', ENT_QUOTES, "utf-8"),
//'defaultCaption' => '{rating} hearts',
'starCaptions'=>[]
]
]);
}
public function getAverageRating($faq_id)
{
$ratings = FaqRating::find('faq_rating')->asArray()
->where(['faq_id' => $faq_id])
->all();
$ratings =
$ratingsSum = array_sum($ratings);
$ratingsCount = count($ratings);
if($ratingsCount){
$averageRating = $ratingsSum/$ratingsCount;
}
else {
618
619
$averageRating = 0;
}
return $averageRating;
}
}
So that is our change from the boilerplate. Obviously, we added timestamp behavior and the
appropriate use statements to use it. We also modified our rules, removing created_at and
updated_at from being required.
If you dont remove the required rule for those columns, you get hard-to-diagnose validation errors.
This happens when validation fails behind the scenes, not in an obvious way related to the form.
For example, if you dont fill out a required field on a form, you dont get to submit, and it makes it
fairly obvious what the problem is. However, when the problem is not directly related to the form,
things can get tricky.
Lets say for example, you forget to make the table auto-increment and then you use Gii to create the
Model and Crud. Gii will put the id as an input field because its not done automatically. Lets say
you see the id field, not realizing your mistake and you remove that field from the form, thinking it
would be a simple fix.
What would happen in that scenario is that the when you submit the form, you would get a blank
white screen and no record inserted. More importantly, you would get no error message telling you
whats wrong.
$model->getErrors()
Most programmers know how to use var_dump() to help figure out problems, but these validation
errors can be a real pain to sort out. Fortunately, Yii 2 gives us a model method that returns the
errors that you can use like so:
var_dump($model->getErrors()):
die();
620
}
}
You would add this method to whichever model you are trying to troubleshoot.
Just remember that after you use this to diagnose a problem that you should remove it in order to
restore full functionality.
Anyway, those are some tips on how to overcome validation errors that are not otherwise visible to
you.
Ok, lets get back to our new model, FaqRatings. In addition to the changes we already talked about,
we have two new methods, showAverageRating and getAverageRating. Lets discuss the second one
first.
public function getAverageRating($faq_id)
{
$ratings = FaqRating::find('faq_rating')->asArray()
->where(['faq_id' => $faq_id])
->all();
$ratings =
$ratingsSum = array_sum($ratings);
621
$ratingsCount = count($ratings);
if($ratingsCount){
$averageRating = $ratingsSum/$ratingsCount;
}
else {
$averageRating = 0;
}
return $averageRating;
}
You can see we need the id of the faq in the signature, then we use ActiveRecord to get all ratings that
have an faq_id equal to the faq_id that we handed in. Were taking the results as an array because
its easy to work with these values in this format.
Since we have all our ratings stored in our $ratings array, we use ArrayHelper::map to filter
everything out except the two values we want, id and faq_rating. So now we can use:
$ratingsSum = array_sum($ratings);
This will add up the total value of all ratings, which we can then divide by the number of ratings,
for which we need to count the ratings array:
$ratingsCount = count($ratings);
Then we check to see if there is a count, and if so do the calculation to return the average:
if($ratingsCount){
$averageRating = $ratingsSum/$ratingsCount;
}
else {
$averageRating = 0;
}
return $averageRating;
622
echo StarRating::widget([
'name' => 'rating_' . $averageRating,
'value' => $averageRating,
'disabled' => true,
'pluginOptions' => [
'size' => 'sm',
'stars' => 5,
'min' => 0,
'max' => 5,
'step' => 0.5,
'starCaptions'=>[]
]
]);
}
In order to show the average rating, we need to get the average rating first, which is why we built
the other method first. Once we have the average rating, we can simply plug it into the widget in
the appropriate places, as shown above.
I dont usually echo a widget within a model method, but I did so this time to make things easy
on us. From the Faq controller, Ill pass an instance of the FaqRatings model, which will make this
method available to the view.
Some things to note about the widget. We can set the size to small via sm, the number of stars, in
this case 5, and the step, which indicates a partial value on the star. You can reference Krajee.com if
you want to know more about what settings you can use.
Faq Controller
We need to modify the view action as follows.
Gist:
Frontend Faqcontroller View Action
From book:
if ($slug == $model->slug){
return $this->render('view', [
'model' => $model,
'slug' => $model->slug,
'faqRating' => $faqRating,
]);
} else {
throw new NotFoundHttpException('The requested Faq does not exist.');
}
}
You can see all we did was make a new FaqRating instance available to the view.
623
<div class="panel-heading">
<h3 class="panel-title">
<h1>
</h3>
</div>
<?= '<div class="panel-body"><h3>'. $model->faq_answer .'</h3></div>';?>
</div>
<?php
if (Yii::$app->getSession()->hasFlash('success')){
echo Growl::widget([
'type' => Growl::TYPE_SUCCESS,
'title' => 'Thank you!',
'icon' => 'glyphicon glyphicon-ok-sign',
'body' => Yii::$app->session->getFlash('success'),
'showSeparator' => true,
'delay' => 0,
'pluginOptions' => [
'placement' => [
'from' => 'top',
'align' => 'right',
]
]
]);
}
Yii::$app->getSession()->removeFlash('success');
?>
<div id="showAverage">
<strong> Faq Rating
<?php
</strong>
$faqRating->showAverageRating($model->id);
?>
<br>
624
Ok, so we added some new things here. Right below the faq answer, we added a growl widget:
<?php
if (Yii::$app->getSession()->hasFlash('success')){
echo Growl::widget([
'type' => Growl::TYPE_SUCCESS,
'title' => 'Thank you!',
'icon' => 'glyphicon glyphicon-ok-sign',
'body' => Yii::$app->session->getFlash('success'),
'showSeparator' => true,
'delay' => 0,
'pluginOptions' => [
'placement' => [
625
626
What the Growl::widget does is format a flash message into an animated message. I thought it added
a nice touch to the UI and the widget is already part of the Kartik widget extension, so we already
had everything we need for it. We will discuss it more in detail when we work on the controller.
Next we have the call to the showAverageRatings method:
<div id="showAverage">
<strong> Faq Rating
<?php
</strong>
$faqRating->showAverageRating($model->id);
?>
<br>
<button type="button" id="rateMe" class="btn btn-default">
Add Your Rating
</button>
</div>
You can see that we added a div id of showAverage. We also have a button within the div with an
id of rateMe.
Next we have a call to a form partial, which will allow us to submit the rating:
<div id="rateIt">
<?php
echo $this->render('_rating-form', ['model'=> $model,
'faqRating' => $faqRating]);
?>
</div>
627
Even without seeing the form yet, you can see that it should render the form, which is odd because
we are also displaying the average rating. And yet from our screenshot, we know that it is only
supposed to show the form when the add your rating button is clicked.
That brings us to our next section. We manage this by using show/hide in jquery:
<?Php
$script = <<< JS
$(document).ready(function(){
$("#rateIt").hide();
$("#rateMe").click(function(){
$("#showAverage").hide();
$("#rateIt").show();
});
});
JS;
$this->registerJs($script);
?>
You can google heredoc if you are unfamiliar with that notation. Its a very convenient way to
integrate PHP and javascript. At the end, we use:
$this->registerJs($script);
Then when someone clicks the Add Your Rating button, which has an id of rateMe, then we hide
showAverage and we show the rateIt div:
628
$("#rateMe").click(function(){
$("#showAverage").hide();
$("#rateIt").show();
});
You dont have to worry about getting back to showAverage from this point because the controller
that processes our _rating-form will take care of that.
So this is a demonstration of how to embed javascript directly in the page. The other way to go is
to put the file in a js folder under frontend/web and reference it in the AppAsset.php file.
_rating-form.php
Since were calling _rating-form within this view, create that now. This file should go in the
frontend/views/faq folder.
Gist:
_rating-form.php
From book:
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
use kartik\widgets\StarRating;
/* @var $this yii\web\View */
/* @var $model backend\models\FaqRating */
/* @var $form yii\widgets\ActiveForm */
?>
<div class="faq-rating-form">
<?php $form = ActiveForm::begin([
'method' => 'post',
'action' => ['faq-rating/rating'],
]); ?>
<?= Html::activeHiddenInput($faqRating, 'faq_id',
['value' => $model->id]) ?>
<?= $form->field($faqRating, 'faq_rating')
629
Most of the forms we have done so far did not require us to set the action. But this one is not being
processed by the FaqController, even though the form is sitting in the faq view folder.
Instead, we are sending this form to the FaqRatings Controller, which we have not created yet.
Ok, back to the form. After setting the action, we are using the StarRating widget for the form field:
630
This will take in the value the user gives it when they select their star rating and submit via the form
when the submit button is pressed.
<div class="form-group">
<?= Html::submitButton('Rate It!', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>
Were using the Html helper submitButton method, and we give it a label, Rate It!, and a style. Note
that this is different from what we have seen in most of our forms, something like:
<?= Html::submitButton($model->isNewRecord ? 'Create' :
'Update', ['class' => $model->isNewRecord ?
'btn btn-success' : 'btn btn-primary']) ?>
In the above example, we use the $model->isNewRecord method to determine if its a new record
or not, and then show the appropriate button.
I couldnt get $model->isNewRecord to work correctly in this case, and once I started to work around
it, I realized this would be a good example for how to do it when that method is not available. This
has implications in the controller, as we will see shortly.
FaqRatings Controller
We need to create the controller, and in this case, Gii is not going to be helpful, so you can just copy
this file.
Gist:
FaqRatings Controller
From book:
<?php
namespace frontend\controllers;
use
use
use
use
use
use
use
use
Yii;
backend\models\FaqRating;
yii\web\Controller;
yii\web\NotFoundHttpException;
yii\filters\VerbFilter;
backend\models\Faq;
yii\helpers\Html;
yii\helpers\Url;
/**
* FaqRatingController implements the CRUD actions
* for FaqRating model.
*/
class FaqRatingController extends Controller
{
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
];
}
/**
* Creates a new FaqRating model.
* If creation is successful, the browser will
* be redirected to
* the 'view' page.
* @return mixed
631
*/
public function actionRating()
{
if(Yii::$app->user->isGuest){
return $this->redirect(['site/login']);
}
$model = new FaqRating();
$model->user_id = (int) Yii::$app->user->identity->id;
if ($model->load(Yii::$app->request->post())) {
$existingRating = FaqRating::find()
->where(['user_id' => $model->user_id])
->andWhere(['faq_id' => $model->faq_id])
->one();
if(isset($existingRating->id)){
$existingRating->faq_rating = $model->faq_rating;
$existingRating->update();
$slug = Faq::find('slug')
->where(['id' => $existingRating->faq_id])
->one();
Yii::$app->session->setFlash('success',
'Thank you for updating this Faq to '
.$existingRating->faq_rating.
' stars. Your result is factored into the
average.');
return $this->redirect(['faq/view',
'id' => $existingRating->faq_id,
'slug' => $slug->slug
]);
632
} else {
if($model->save()) {
$slug = Faq::find('slug')
->where(['id' => $model->faq_id])
->one();
Yii::$app->session->setFlash('success',
'Thank you for rating this Faq ' .$model->faq_rating.
' stars. Your result is factored into the average.');
return $this->redirect(['faq/view',
'id' => $model->faq_id, 'slug' => $slug->slug
]);
}
}
} else {
throw new NotFoundHttpException('There was a problem');
}
}
}
633
634
By only allowing delete on post, we put up another defense against delete via url manipulation.
If you dont specify this, someone might be able to manipulate a get variable in the url to delete
records, which is obviously not good.
In this case, specifying post probably isnt necessary, since we dont have a delete action, but Ill
leave it in just as a precaution.
The only other method we have in the controller is the actionRating method. Lets look at the first
part:
public function actionRating()
{
if(Yii::$app->user->isGuest){
return $this->redirect(['site/login']);
}
This simply requires login by the user in order to submit a rating. By doing so, we dont allow a user
create multiple rating records for a single Faq, since the user must be logged in and we can check to
see if they have already rated it.
Next instantiate a new FaqRating model, so we can assign the user_id property to the currently
logged in user, since were not handing that in with the form:
$model = new FaqRating();
$model->user_id = (int) Yii::$app->user->identity->id;
The (int) specifies that we want an int out of Yii::$app->user->identity->id. I did that because when
I var_dumped to test, I got string as an answer. The validation requires an int however.
Next we load the post data and check to see if there is a matching record that already exists:
635
if ($model->load(Yii::$app->request->post())) {
$existingRating = FaqRating::find()
->where(['user_id' => $model->user_id])
->andWhere(['faq_id' => $model->faq_id])
->one();
If it does already exist, we assign the rating from the form to this instance of the model, which is
$existingRating, and then we run update:
if(isset($existingRating->id)){
$existingRating->faq_rating = $model->faq_rating;
$existingRating->update();
Note that we cant just run save because it will create a new record for us. This is a byproduct of
not being able to use $model->isNewRecord in the form.
Next, because our Faq view pages require a slug to resolve, we need to look up the appropriate slug:
$slug = Faq::find('slug')
->where(['id' => $existingRating->faq_id])
->one();
Were making this flash message dynamic so it will pass back the updated rating, which we will
include in the message.
Finally, we redirect to the correct view, passing the id of the faq and the slug:
636
return $this->redirect(['faq/view',
'id' => $existingRating->faq_id,
'slug' => $slug->slug
]);
If it was not an existing record, we have a simpler path. We just save, find the slug, set the flash,
and redirect:
} else {
if($model->save()) {
$slug = Faq::find('slug')
->where(['id' => $model->faq_id])
->one();
Yii::$app->session->setFlash('success',
'Thank you for rating this Faq ' .$model->faq_rating.
' stars. Your result is factored into the average.');
return $this->redirect(['faq/view',
'id' => $model->faq_id, 'slug' => $slug->slug
]);
}
}
You should be able to test that at this point and it all should work.
Lets return to the view for a minute to discuss the Growl widget:
<?php
if (Yii::$app->getSession()->hasFlash('success')){
echo Growl::widget([
'type' => Growl::TYPE_SUCCESS,
'title' => 'Thank you!',
'icon' => 'glyphicon glyphicon-ok-sign',
'body' => Yii::$app->session->getFlash('success'),
'showSeparator' => true,
637
'delay' => 0,
'pluginOptions' => [
'placement' => [
'from' => 'top',
'align' => 'right',
]
]
]);
}
Yii::$app->getSession()->removeFlash('success');
?>
In the body setting, we pull in our specific flash message, which has been set in the controller:
'body' => Yii::$app->session->getFlash('success'),
Obviously you can change this to handle error messages as well. I didnt feel it was necessary
because if there is an error, we throw an exception in the controller. You can check Kartiks widget
documentation for more details on settings.
Youll also note that we add a separate line after the widget fires:
Yii::$app->getSession()->removeFlash('success');
This prevents the default behavior, which would be a Bootstrap alert element containing the flash
message, which would fire in addition to the growl.
If you wanted to code this where you didnt want someone to be able to update their rating, you
would code that logic in here:
638
if(isset($existingRating->id)){
// redirect with flash message
// sorry, you are not allowed to update your rating
So we have a nice frontend implementation of our FaqRatings model, but we probably want to know
the ratings for individual Faqs in the backend too.
Since weve already done all the heavy lifting, this will be easy to implement. We have just 3 changes.
Faq Model
We need to add to the model a method to return the ratings of each Faq. Lets add the following:
public function getFaqRatings($id)
{
$rating = new FaqRating;
return $rating->getAverageRating($id) ?
$rating->getAverageRating($id) : 'Not Rated' ;
}
We already have a method for getting the average rating on the FaqRating model, so we just use
that to return the average rating of our Faq. If its null or 0, well set it to Not Rated.
Troubleshooting tip: Please make sure you added that to the Faq model. We have a number of models
with Faq in the name, so watch out for that.
639
Faq View
Finally, we add one line to the DetailView widget in view.php:
['attribute'=>'Rating', 'format'=>'raw',
'value' => $model->getFaqRatings($model->id)
],
And thats it. I dont have a solution yet for making the rating column sortable, but Im working on
it.
Its complicated because we are using the setSort method of ActiveDataProvider, and that is set up to
order DB columns and our ratings column is obviously not that. I will probably have to use a fairly
complicated query with either raw SQL or by creating a custom query class to get what I need. Its a
subject worthy of its own chapter, but I didnt want to hold up publication for that, so I will update
the book when I have that solution in place.
Ok, lets move on.
When users join a website, they typically have to agree to the terms of service. This is often
represented as a checkbox, and if you dont agree, you dont get to signup.
Typically, we also see the terms of service in a scrollable box near the check box, like so:
Modify SignupForm model with the new checkbox attribute and validation
Modify signup.php view file to contain the checkbox and render terms
Create terms.php view file to hold the terms content
Create overflow css style to define scroll bars
Modify AppAsset.php to hold the new css style.
640
Gist:
SignupForm.php
From book:
<?php
namespace frontend\models;
use common\models\User;
use yii\base\Model;
use Yii;
/**
* Signup form
*/
class SignupForm extends Model
{
public $username;
public $email;
public $password;
public $agreeToTerms;
/**
* @inheritdoc
*/
public function rules()
{
return [
['username', 'filter', 'filter' => 'trim'],
['username', 'required'],
['username', 'unique', 'targetClass' => '\common\models\User',
'message' => 'This username has already been taken.'],
['username', 'string', 'min' => 2, 'max' => 255],
['email', 'filter', 'filter' => 'trim'],
['email', 'required'],
['email', 'email'],
['email', 'unique', 'targetClass' => '\common\models\User',
'message' => 'This email address has already been taken.'],
['password', 'required'],
['password', 'string', 'min' => 6],
641
642
['agreeToTerms', 'boolean'],
['agreeToTerms', 'compare', 'compareValue'=>true,
'message' =>'You must agree to our Terms of Service.'],
];
}
/**
* Signs user up.
*
* @return User|null the saved model or null if saving fails
*/
public function signup()
{
if ($this->validate()) {
$user = new User();
$user->username = $this->username;
$user->email = $this->email;
$user->setPassword($this->password);
$user->generateAuthKey();
if ($user->save()) {
return $user;
}
}
return null;
}
}
So were defining it as a boolean. Then we use the compare validator to make sure the value is true,
which means the checkbox has been checked. We also set the error message if the compare value
fails.
signup.php
Gist:
signup.php
From book:
<?php
use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
/* @var $this yii\web\View */
/* @var $form yii\bootstrap\ActiveForm */
/* @var $model \frontend\models\SignupForm */
$this->title = 'Signup';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="site-signup">
<h1><?= Html::encode($this->title) ?></h1>
<br>
<?= yii\authclient\widgets\AuthChoice::widget([
'baseAuthUrl' => ['site/auth'],
'popupMode' => false,
]) ?>
<p>Otherwise please fill out the following fields to signup:</p>
<div class="row">
<div class="col-lg-5">
<?php $form = ActiveForm::begin(['id' => 'form-signup']); ?>
<?= $form->field($model, 'username') ?>
<?= $form->field($model, 'email') ?>
<?= $form->field($model, 'password')->passwordInput() ?>
643
644
<br>
</div>
<br>
<div class="row">
<div class="col-sm-4">
<?= $form->field($model, 'agreeToTerms')->checkbox() ?>
<div class="form-group">
<?= Html::submitButton('Signup', ['class' =>
'btn btn-primary', 'name' => 'signup-button']) ?>
</div>
</div>
</div>
<?php ActiveForm::end(); ?>
</div>
</div>
</div>
You can see how we are rendering the terms.php view, which we have not created yet. @app is a
path alias that we need so the renderFile method knows where to look for the file.
Next we have:
645
<div class="row">
<div class="col-sm-4">
<?= $form->field($model, 'agreeToTerms')->checkbox() ?>
<div class="form-group">
<?= Html::submitButton('Signup', ['class' => 'btn btn-primary',
'name' => 'signup-button']) ?>
</div>
</div>
</div>
We are getting Agree To Terms as a default label generated by the form field. If you want to change
it, use the label method. For example:
<?= $form->field($model, 'agreeToTerms')
->label('I Agree To Terms')->checkbox() ?>
Also note:
<div class="row">
<div class="col-sm-4">
We need that to format the css. The scroll bars will not be added, however, until we define a style
for them. We will do that after the next step.
terms.php
Lets create terms.php in the frontend/views/pages folder. The reason we are putting it here is that so
later on we can add it in other places on the application, perhaps a footer, to make it more accessible.
Gist:
terms.php
From book:
<h1>Terms Of Service</h1>
646
647
malesuada eu risus vitae tristique. Mauris at dolor blandit, ullamcorper ante eu\
,
mollis elit. Fusce ultrices ligula ut tellus semper, ut ullamcorper sem tristiqu\
e.
Mauris blandit, urna at pulvinar efficitur, elit eros hendrerit urna, nec fringi\
lla
leo metus sit amet odio. Donec eu ultrices nisl. Quisque eu enim porta,
fringilla metus non, bibendum tortor. Cras eu egestas enim. Aenean interdum,
eros sed pretium rutrum, nisi augue hendrerit enim, dictum feugiat est augue
vitae est. Donec vitae sagittis eros. Duis congue, eros vel pellentesque
molestie, justo turpis facilisis felis, mollis semper velit sem a nunc.
Integer eleifend, tellus sit amet auctor bibendum, quam leo maximus est,
eget feugiat velit nulla eget mauris.</p>
<p><strong>5.</strong> Fusce vel dolor eget ex varius porta quis ac libero.
Proin eget metus egestas, pretium mi non, dictum nisi. Cras fringilla varius
massa vitae convallis. Nunc ut blandit odio. Sed vehicula felis in neque
auctor hendrerit. Nunc a magna eget felis interdum auctor. Aliquam
accumsan metus justo, eget iaculis diam pharetra nec. Donec mauris lacus,
lacinia non venenatis in, interdum at purus. Donec ac mi id tortor mattis
convallis id sed tellus. Nullam volutpat justo pretium odio eleifend, ac
vestibulum lectus tempus. Quisque ut sem viverra lorem facilisis facilisis.
Aliquam erat volutpat.
</p>
Obviously not much to that. I will mention that I used the lorem ipsum generator at:
Lorem Ipsum Generator
termsoverflow.css
To get the scroll bars to work correctly, we need to create termsoverflow.css and add it to the
frontend/web/css folder.
This class consists of the following:
648
#terms {
height: 150px;
overflow: scroll;
}
Frontend AppAsset.php
Now that we have our style class to show the scroll bars, we need to tell our app to use it. We need
to modify the our frontend AppAsset.php
Gist:
Frontend AppAsset.php
From book:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace frontend\assets;
use yii\web\AssetBundle;
/**
* @author Qiang Xue <[email protected]>
* @since 2.0
*/
class AppAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
'css/site.css',
'css/termsoverflow.css'
];
public $js = [
];
649
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap\BootstrapAsset',
];
}
That was just a simple addition, adding our new style to the public $css array.
That should be everything you need for this implementation.
Modify marketing_image Table with a new field for mobile image path.
Modify MarketingImage model to account for the new property, rules and scenario.
Modify various views including the form to allow the upload of the additional image.
Modify MarketingImageController create, update, and delete actions for new image type.
Modify defaults in CaourselWidget.php to allow for no width or height to be set
Modify carousel.php to show the appropriate image based on browser size via jquery.
Modify CarouselSettings model to make height and width of the image not required.
Modify marketing_imageTable
Ok, lets jump in. Here is the SQL to alter the marketing_image table:
ALTER TABLE `yii2build`.`marketing_image`
ADD COLUMN `marketing_mobile_path` VARCHAR(45)
CHARACTER SET 'utf8' COLLATE 'utf8_unicode_ci'
NOT NULL AFTER `marketing_thumb_path`;
650
651
'range'=>array_keys($this->getMarketingImageIsActiveList())],
[['file'], 'file', 'extensions' => ['png', 'jpg',
'gif', 'jpeg'], 'maxSize' => 1024*1024],
[['mobileFile'], 'file', 'extensions' => ['png', 'jpg',
'gif', 'jpeg'], 'maxSize' => 250*250],
[['marketing_image_path', 'marketing_image_name'],
'string', 'max' => 45],
[['marketing_image_caption', 'marketing_image_caption_title'],
'string', 'max' => 100],
];
}
You should note that I set the size limit on the mobileFile to 250 x250. You can change this if you
prefer a different size in the mobile image.
Next, add into scenarios marketing_mobile_path.
Gist:
scenarios
From book:
public function scenarios()
{
$scenarios = parent::scenarios();
$scenarios['create'] = ['file','marketing_image_path',
'marketing_image_name', 'marketing_thumb_path',
'marketing_mobile_path', 'marketing_image_is_featured',
'marketing_image_is_active', 'marketing_image_caption',
'marketing_image_caption_title', 'marketing_image_weight' ];
return $scenarios;
}
For reference, Im going to give you a Gist for the complete file:
Gist:
MarketingImage.php
MarketingImage Views
Ok, moving on to the views. Lets start with _form.php. We just need to add a single line:
<?= $form->field($model, 'mobileFile')->label('Mobile Image')->fileInput(); ?>
652
653
654
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'marketing_image_caption_title',
'marketing_image_caption',
'marketing_image_path',
'marketing_thumb_path',
'marketing_mobile_path',
'marketing_image_weight',
['attribute' => 'marketing_image_is_featured',
'format' => 'boolean'],
['attribute' => 'marketing_image_is_active',
'format' => 'boolean'],
'status.status_name',
'created_at',
'updated_at',
],
]) ?>
</div>
And of course we added the image itself, using our get variable to prevent caching:
<?php
echo Html::img('/'. $model->marketing_mobile_path .
'?'. 'time='. time(), [
'alt' => $model->marketing_image_name]);
?>
Update View
We also add the mobile image and titles to update.php.
Gist:
Update View
From book:
<?php
use yii\helpers\Html;
/* @var $this yii\web\View */
/* @var $model backend\models\MarketingImage */
$this->title = 'Update Marketing Image: ' . ' ' . $model->id;
$this->params['breadcrumbs'][] = ['label' =>
'Marketing Images', 'url' => ['index']];
$this->params['breadcrumbs'][] = ['label' => $model->id,
'url' => ['view', 'id' => $model->id]];
$this->params['breadcrumbs'][] = 'Update';
?>
<div class="marketing-image-update">
<h1><?= Html::encode($this->title) ?></h1>
<br>
<div>
<h3>Primary Image:</h3>
<?php
echo Html::img('/'. $model->marketing_image_path,
['width' => '600px']);
?>
</div>
<br>
<div>
<h3>Mobile Image:</h3>
<?php
echo Html::img('/'. $model->marketing_mobile_path .
'?'. 'time='. time());
?>
</div>
655
656
<br>
<div>
<h3>Thumbnail Image:</h3>
<?php
echo Html::img('/'. $model->marketing_thumb_path .
'?'. 'time='. time());
?>
</div>
<br>
<?= $this->render('_form', [
'model' => $model,
]) ?>
</div>
MarketingImage Controller
On MarketingImageController, we modified create, update and delete to account for our new
$mobileFile. Lets look at these one at a time.
Create Action
Gist:
create action
From book:
public function actionCreate()
{
$model = new MarketingImage();
$model->scenario = 'create';
if ($model->load(Yii::$app->request->post())) {
$imageName = $model->marketing_image_name;
$model->file = UploadedFile::getInstance($model, 'file');
You can see that we have used getInstance set the uploaded file to $model->mobileFile.
We also have a block to set the name and remove spaces:
$mobileName = 'uploads/' . 'mobile/' . $imageName . '-mobile.' .
$model->mobileFile->extension;
$mobileName = preg_replace('/\s+/', '', $mobileName);
657
$model->marketing_mobile_path = $mobileName;
Update Action
Gist:
update Action
From book:
public function actionUpdate($id)
{
$model = $this->findModel($id);
if ($model->load(Yii::$app->request->post())) {
$imageName = $model->marketing_image_name;
$oldImage = MarketingImage::find('marketing_image_name')
->where(['id' => $id])
->one();
if ($oldImage->marketing_image_name != $imageName){
throw new ForbiddenHttpException
('You cannot change the name, you must delete instead.');
}
if ( $model->file = UploadedFile::getInstance($model, 'file')){
$thumbName = 'uploads/' . 'thumbnail/' . $imageName .
'-thumb.' . $model->file->extension;
}
if ($model->mobileFile = UploadedFile::getInstance($model, 'mobileFile')){
658
if ($model->file) {
$fileName = 'uploads/' . $imageName . '.' . $model->file->extension;
$model->file->saveAs($fileName);
Image::thumbnail( $fileName , 60, 60)
->save($thumbName, ['quality' => 50]);
}
if ($model->mobileFile) {
$model->mobileFile->saveAs($mobileName);
}
return $this->redirect(['view', 'id' => $model->id]);
} else {
return $this->render('update', [
'model' => $model,
]);
}
}
Just two spots to note. The first is before we save the model:
659
660
This is just like what we do for the thumbnail image, so we have already covered this in the previous
chapter.
Next we add:
if ($model->mobileFile) {
$model->mobileFile->saveAs($mobileName);
}
Note that both of these statements are wrapped in if statements because updating the mobile image
is not required, whereas when we create the record, the mobile image is required.
Delete Action
Gist:
delete Action
From book:
public function actionDelete($id)
{
$model = $this->findModel($id);
try {
unlink($model->marketing_image_path);
unlink($model->marketing_thumb_path);
unlink($model->marketing_mobile_path);
$model->delete();
661
return $this->redirect(['index']);;
}
catch(\Exception $e) {
throw new NotFoundHttpException($e->getMessage());
}
}
Ok, thats simple enough. We just added the extra unlink for the mobile image.
Entire File
Im supplying a gist of the entire file for reference, should you need it:
Gist:
MarketingImage Controller
662
CarouselWidget
Moving on to the CarouselWidget class, we are going to change the setDefaults method to set height
and width to null if no other value is provided. This default will be the typical implementation.
I dont need to give you a Gist, just change the value to null as below:
if (!isset($this->settings['height'])){
$this->settings['height'] = null;
}
if (!isset($this->settings['width'])){
$this->settings['width'] = null;
}
663
validateSize Method
Next we wrap everything in the validateSize method in an if statement, so that we are only validating
size if height and width are not empty:
if (!empty($this->settings['width']) && !empty($this->settings['height'])){
}
carousel.php
The last step in this is to modify carousel.php. Ultimately, we are going to do something similar to
what we did with the faq rating, where we show/hide based, in this case, on the size of the browser.
Unfortunately, that means we have to add a second carousel to the view, which takes a giant plate
of spaghetti and doubles it. Theres not much I can do about that. I tried simply replacing part of the
carousel, but that didnt work.
What I did do, that actually worked, is wrap each carousel in a div, one named big and the other
named small. So hopefully that will keep everything clear.
The other big difference is the path to the image in the small carousel. All that is followed by the
javascript at the bottom of the file. Here is the entire file:
Gist:
carousel.php
From book:
<?php
use yii\helpers\Html;
?>
<div id="big">
<div id="carouselMain" class="carousel slide"
<?php
if($settings['autoplay'] == false ){
echo 'data-interval="false"';
}
?>
data-ride="carousel">
<!-- Indicators -->
<?php
if ($settings['show_indicators']){
echo '<ol class="carousel-indicators">
<li data-target="#carouselMain" data-slide-to="0"
class="active"></li>';
foreach (range(1, $count) as $number) {
echo '<li data-target="#carouselMain"
data-slide-to="'.$number.'"></li>';
}
echo '</ol> ';
}
?>
<!-- Wrapper for slides -->
664
665
. '</center>';
if($settings['show_captions']){
echo '<div class="carousel-caption">';
if ($settings['show_caption_title']){
echo
'<div><h1>' .
$image['marketing_image_caption_title'].
'</h1></div>';
}
echo $image['marketing_image_caption'].' </div>';
}
echo '</div>';
}
?>
<!-- end dynamic slide data -->
</div>
<!-- Controls -->
<?php
if ($settings['show_controls']){
666
?>
</div>
</div>
<div id="small">
<div id="carouselSmall" class="carousel slide"
<?php
if($settings['autoplay'] == false ){
echo 'data-interval="false"';
}
?>
data-ride="carousel">
<!-- Indicators -->
<?php
if ($settings['show_indicators']){
echo '<ol class="carousel-indicators">
<li data-target="#carouselSmall" data-slide-to="0"
class="active"></li>';
foreach (range(1, $count) as $number) {
echo '<li data-target="#carouselSmall"
data-slide-to="'.$number.'"></li>';
}
667
668
669
?>
</div>
</div>
<?Php
$script = <<< JS
if ($(window).width() <= 800){
$("#big").hide();
$("#small").show();
$('#carouselSmall').carousel({
interval: 1000
});
}else{
if ($(window).width() >800){
$("#small").hide();
$("#big").show();
$('#carouselMain').carousel();
interval: 1000
}
}
$(window).resize(function(){
if ($(window).width() <= 800){
$("#big").hide();
670
671
$("#small").show();
$('#carouselSmall').carousel({
interval: 1000
});
}else{
if ($(window).width() >800){
$("#small").hide();
$("#big").show();
$('#carouselMain').carousel();
interval: 1000
}
}
});
JS;
$this->registerJs($script);
?>
We have two carousels, each in a separate div, one named big, the other small.
The carousels also have div ids as carouselMain and carouselSmall
The path to the image in carouselSmall is obviously different than that of carouselMain.
The javascript at the bottom of the file determines which carousel will be visible, depending
on browser size.
Summary
In this chapter, we got to build a ratings system for our Faqs, using Kartiks widget extension. That
worked out really well for us because now we know how to create a rating system for any type of
model that users might be willing to rate.
We also implemented the Growl widget, which gave us a nice UI experience, whenever there is a
save or update to the ratings. You can use the Growl widget often, whenever you are setting flash
messages, and it would make the application act in a very consistent way.
We also had our first use of a checkbox, which we implemented to confirm the users agreement to
terms of service. It was simple stuff, but we also added to our frontend AppAssets.php file to pull in
the css, so we are getting more familiar with Yii 2s asset publishing.
672
Finally, we went back into the carousel to give ourselves full control over that marketing space,
so we can nuance the image in a mobile browser by using a separate image for that purpose. Even
though we expanded the functionality of the carousel, we still have 100% management of the carousel
through our backend UI, creating a user-friendly admin environment that will be appreciated by
those responsible for the marketing in the carousel. And thats it for this chapter.
Thanks once again to everyone for the positive comments and reviews, they keep me motivated to
keep going with more bonus material. Please help spread the word about the book if you can. It will
be greatly appreciated.
In the meantime, I will continue to work on bonus material and I will keep working on the template.
Thanks again for supporting the book. See you soon.
674
Extended Table
Its hard to see everything because its compressed down, but we get the extra row we need for
average rating. I based this query on a tutorial:
Query Tutorial
You can see this isnt the friendliest format to work with. For one thing, the above query is not
returning results when there is no rating, so the query is not even correct.
The simple solution to that problem is to make it a LEFT join, which will return all the results. Ok,
so we overcame that.
When I first started using Yii 2, I tended to rely more on plain SQL than ActiveRecord, for the reason
illustrated above. If you need to figure out a query, you can simply search for an answer, and it can
be faster to develop that way. But now that I have more experience with ActiveRecord, I really love
it. Its so much more intuitive and easy to use. Its worth taking the extra time to figure out how to
work with it.
There is something else to consider, however, and that is the overhead you will incur by using any
frameworks ORM. For large databases and complex queries, its generally not considered practical
or scalable. But with PHP 7 coming, the framework overhead might be reduced to a very workable
level for complex queries. And that means we can use ActiveRecord without worrying that we are
harming our ability to scale.
Since PHP 7 is due in October of this year, Im going on the assumption that ActiveRecord will be a
viable solution complex queries.
Also, as we will see as we develop this, we do not have to sacrifice Mysqls built-in functions like
AVG and COUNT when we use ActiveRecord, so we still get to benefit from MySqls performance
capabilities for those kinds of queries.
So now we have to figure out how to translate our SQL query to ActiveRecord, if we want to use it
with ActiveDataProvider and continue to use the existing solution we have for our FaqSearch search
method. This is what we want to do.
The problem is that we have raw SQL, but we dont know how to translate it to ActiveRecord, or
even if it can be translated to ActiveRecord.
675
To figure this out, we should reference two sections of the Yii 2 docs:
Query Builder
And
ActiveRecord
Theres quite a lot of information there on all the methods available to us. That said, I was still a
little unsure. So I went to the forum for help:
Help with SQL to ActiveRecord
Very quickly I received a couple of responses, and one of those was from Kartik. It turns out, he has
written a web tip on his site for this scenario:
Kartiks Web Tip
So, in addition to all the amazing widgets he has made for us, he also has anticipated that we would
need some help with sorting a calculated field in Gridview. Its just incredibly helpful.
Donate To Kartik
Please donate to Kartik if you can, he is contributing a lot to the Yii 2 community. His donation
button is at:
Krajee.com
Here is a copy of my donation:
Donation
Its a great way to say thanks for all the hard work he puts into his extensions and tutorials, which
he actively maintains.
676
Ive described my research into this chapter fully, so you can understand how to approach this if
you find yourself needing help with ActiveRecord or anything else.
You should always research thoroughly before going to the forum because theres no sense in asking
a question thats already been answered. I made the mistake of focusing on the query, not thinking
the full solution was out there, so I never saw Kartiks web tip. Next time I will try searching on the
full solution before going to the forum.
Anyway, lets jump into this solution.
So this is just a relationship, with the average method added on. Simple, but powerful.
I found the right syntax for average by referencing the query builder section of the docs. Well see
later that the average method inside of MySql is AVG, so we have to be sure to use the right syntax
in the right place.
During my research for this chapter, I also explored the possibility of creating a scope instead of the
above method. However, scopes are not directly supported by Yii 2. Instead, you can create a custom
query class that overrides your find method.
After looking at the two solutions, I felt the one offered by Kartiks tutorial was simpler. It is also
listed in the Yii 2 guide as an alternative to creating a scope. So, all other things being equal, I chose
the simplest implementation.
If I find a good use case for the custom query class on the template, I will include it in a future
chapter.
Ok, back to the base model. We also add an attributeLabel:
677
This isnt anything we havent seen before. In case you need a reminder, we use the magic syntax,
so there is no get and the average is lowercase since its the first word.
Now well move on to the search model FaqSearch. Lets start with a use statement:
use backend\models\FaqRating;
All we did was add the single attribute averageRating to the safe rule.
Ok, moving on to our search method, we start by adding to our query definition. Before we had this:
$query = Faq::find();
678
$query = Faq::find();
$subQuery = FaqRating::find()
->select('faq_id, AVG(`faq_rating`) as average_rating')
->groupBy('faq_id');
$query->leftJoin([
'faqAverage'=>$subQuery
], 'faqAverage.faq_id = faq.id');
You can see we created a subquery and did a left join. In the subquery, we are selecting AVG(faq_rating), which is the MySql function to calculate an average.
You can also see that we give the $subQuery variable a name in the leftJoin array, in this case its
faqAverage. And right away you can see that this alias works in the on part of the join. So instead
of faq_rating.faq_id, we have faqAverage.faq_id to join on faq.id.
Doing it this way means that the AVG function will loop for each instance of FaqRating where
faq_rating.faq_id is equal to faq.id. And this is what were looking for.
The only thing I dont like about it is that we have already defined the getAverageRating method
on the Faq model, so this seems a little redundant. I tested it to see if we needed both and it turns
out that we do.
As far as I can tell, the sorting capability we want relies on this subQuery, and the actual line by
line results rely on the model method. If we drop anything, it will not work.
So moving on to our setSort method, we add the following block.
Gist:
setSort Block
From book:
'averageRating'=>[
'asc'=>['faqAverage.average_rating'=>SORT_ASC],
'desc'=>['faqAverage.average_rating'=>SORT_DESC],
'label'=>'Rating'
],
Remember that averageRating is our getAverageRating method on the base model and faqAverage
is the label we gave our subQuery.
The last piece to pop in the search method is the line to filter by average rating:
679
The way we did this in previous examples was a little more verbose. It would look like this:
$query->joinWith(['faqRating' => function ($q) {
$q->andFilterWhere(['faqAverage.average_rating'=>$this->averageRating]);
}]);
I checked the debug toolbar for DB queries and there was no difference in performance between the
two, so I went with the shorter line.
Ok, so the last little bit is the addition of one line in Gridview in backend/views/faq/index.php:
'averageRating',
I put that on the line immediately following faq_weight. And that should do it. You now have a fully
sortable column for a calculated value, in this the case, the average rating of the faq.
Times Rated
I was curious about how we might add additional calculated values, so I decided to return a
calculated value for the number of times an faq is voted on.
Im going to step through this quickly because most of it is exactly the same as what we just did. So
well start with our Faq model method.
Gist:
getRatingsCount
From book:
public function getRatingsCount()
{
return $this
->hasMany(FaqRating::className(), ['faq_id'=>'id'])
->count('faq_rating');
}
680
Then comes the safe rule, we just add the one attribute:
public function rules()
{
return [
[['id', 'faq_category_id', 'faq_weight', 'faq_is_featured',
'created_by', 'updated_by'], 'integer'],
[['faq_question', 'faq_answer', 'created_at', 'updated_at',
'faqCategoryName', 'faqCategoryList', 'faqIsFeaturedName',
'createdByUsername', 'updatedByUsername', 'faq_category',
'faq_weight', 'averageRating', 'ratingsCount'], 'safe'],
];
}
Ok, so you can see that we simply added to the subQuery instead of creating a new one.
In the subQuery, we are returning count(faq_rating) as times_rated, so we can use times_rated in
our setSort.
That also means we use faqAverage, which identifies the subquery, for this block in the set sort.
681
'ratingsCount'=>[
'asc'=>['faqAverage.times_rated'=>SORT_ASC],
'desc'=>['faqAverage.times_rated'=>SORT_DESC],
'label'=>'Times Rated'
],
And then well simply add the filter line in the appropriate place:
// filter by rating count
$query->andWhere(['faqAverage.times_rated'=>$this->ratingsCount]);
Gridview Screenshot
Im going to give you the Faq model and the FaqSearch model gists for reference in case you need
to troubleshoot:
Faq Model
FaqSearch Model
Summary
Sortable, calculated values are very important to the businesses that manage data through web
applications. Important questions can be answered quickly such as what is the average rating, which
one has the highest rating and which one was rated the most times.
682
The column sorts facilitate these answers, so admins can get to the key data quickly. This allows
companies to follow trend data, which they use to make important decisions.
This was a short focused chapter on returning calculated values in Gridview, a nice way to end the
book. I have had a lot of fun writing this stuff and I hope you found it helpful.
Thanks again to everyone who supports this book by sending in typo notices, leaving positive
comments and positive reviews, I really appreciate it. See you soon.