Hi, this is your host Michał Rudnicki short "stronger", and Ye are visiting my jagged border of life and work. Have a nice read, and please feel free to drop me a line or twelve.
Since this is a tri-lingual blog you may find it useful to filter it out by:
english |
polish |
php-ish
Unobtrusive CSS icons
27 Jul 2010
There can't be anything more straightforward than displaying icons beside links/buttons, can it? After all it's just a CSS class for generic icon with background-image inline style. Or maybe separate CSS class for each icon? After all, there's only so many of them. Or even partial view that takes image, caption, and link as parameters and spits out complete piece of HTML?
None of the above. They all suck due to either inherent ugliness, annoing restrictions, or lack of simplicity. All fail programmers' ultimate criterion – <blink>elegance</blink>. But there is one that excels where others fall short.
Enter convention
<style type="text/css">
.icon { padding: 0 0 0 20px; background: transparent left center no-repeat }
</style>
<a href="#" class="icon i-add">Add</a>
<a href="#" class="icon i-edit">Edit</a>
<a href="#" class="icon i-delete">Delete</a>
A convention whereby all elements with class icon i-something are transformed so that i-something part becomes background-image: url(/icons/something.png).
Nothing can be easier with jQuery.
$(".icon").each(function (i, e) {
var icon = e.className.replace(/.*i-/, '');
e.style.backgroundImage = 'url(/icons/' + icon + '.png)';
});
- Clean - no inline CSS clutter.
- Simple - completely flat learning curve.
- Powerful - no need to define separate CSS class for each icon.
Note: make sure your web server has no alias set for path /icons/ or you won't see any icons.
11 comments | permalink | trackback | rss
Agile database modelling
10 Jan 2010
Things change. New laws are being passed, new business rules are kicking in, new requirements emerge and old ones get phased out. To keep the planets moving software must play catch up and good developer can be easily spotted: their code is ready for changes pretty much all the time and the change won't be a painful one.
NoSQL
The überpopular term NoSQL is not the thing I'm about to propose here. Despite its enthusiastic uptake the established position in the industry is yet to come. Secondly, because there are absolutely outstanding tools, books, common practices, and other knowledge sources on relational databases, SQL language, and Object-Relational Mapping. There are RDBMSes that range from zero to megabucks, there is workforce on the market that range from interns to six-digits-salary wizards. Everywhere you look the web is living and breathing in relational databases.
But I'm getting off the point. To outline this picture quickly, let's put two items into the equation:
- design and implementation details do change
- database schemas are strict and do not automatically adopt to changes by themselves
Granted the two forces opposing, cracks on the surface are inevitable. Sure, you can bend your neck and keep updating schema with every change. Or you could employ one of this big, complicated ORM engines and force it on all memebers of your team. Or, you can oppose all change and get replaced with someone competent. Finally, you can be smart and use in-schema serialised LOBs to provide required dynamic features. To illustrate what I mean, may I present real life example. I use PDO for database access, DAO for persistence handling, and JSON LOBs for dynamic containers. First, example without dynamic features:
CREATE TABLE orders ( id SERIAL PRIMARY KEY, ref_num VARCHAR(60), created_on TIMESTAMP NOT NULL DEFAULT current_timestamp, customer_id INT );
class Order {
protected $columns = array('id', 'ref_num', 'created_on', 'customer_id');
protected $data = array();
protected $dao;
public function __construct() {
$this->dao = new PDODataAccessObject('orders', 'id', $this->columns);
}
public static function getById($id) {
$o = new Order();
$o->data = $o->dao->getByPrimaryKey($id);
return $o;
}
public function __get($k) {
return array_key_exists($k, $this->data) ? $this->data[$k] : null;
}
public function __set($k, $v) {
$this->data[$k] = $v;
}
public function save() {
$this->dao->save($this->data);
}
}
This is minimal implementation of working persistent object. As you can possibly imagine class PDODataAccessObject is a relational storage, which was skipped as not being relevant for this example. You can retrieve Order from database by primary key simply typing $order = Order::getById(10). You can read echo $order->created_on and write $order->ref_num = 12345 object properties, that will be stored in database upon $order->save(). DAO will figure out whether to insert new row into db or update existing – but that's not relevant either. Let's enable class Order for dynamic schema.
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
ref_num VARCHAR(60),
created_on TIMESTAMP NOT NULL DEFAULT current_timestamp,
customer_id INT,
json TEXT
);
Column "json" is a JSON container for dynamic properties.
class Order {
protected $columns = array('id', 'ref_num', 'created_on', 'customer_id', 'json');
protected $data = array();
protected $json;
protected $dao;
public function __construct() {
$this->dao = new PDODataAccessObject('orders', 'id', $this->columns);
}
public static function getById($id) {
$o = new Order();
$o->data = $o->dao->getByPrimaryKey($id);
return $o;
}
public function __get($k) {
if (in_array($k, $this->data)) {
return array_key_exists($k, $this->data) ? $this->data[$k] : null;
}
if (is_null($this->json)) {
$this->json = (array)json_decode($this->data['json'], true);
}
return array_key_exists($k, $this->json) ? $this->json[$k] : null;
}
public function __set($k, $v) {
if (in_array($k, $this->data)) {
$this->data[$k] = $v;
return;
}
if (is_null($this->json)) {
$this->json = (array)json_decode($this->data['json'], true);
}
$this->json[$k] = $v;
}
public function save() {
$this->data['json'] = json_encode($this->json);
$this->dao->save($this->data);
}
}
Parts in bold were added to previous implementation.
Note the additional column json in table schema. This is where your dynamic properties go. Now, you are free to store any property you want without making any change to schema! Like, $order->if_not_delivered_please_call = "085123456"
No DBA would ever design something like this!
Yep, true. It is precisely because what most DBAs tend to care about is their golden temple of normalised tables, non-reduntant schemas, and optimal column sizes. It's the mental wank, the uplifting feeling of doing "Proper Engineering" that hardly ever contributes to anything but self esteem. Such DBAs would fear of a craftsman who treats their golden temple as a mean to an end and not an end itself, a developer who rather than sticking to "middleware" operates across and between layers where needed.
Dynamic schema extension as presented above has severe limitation of not being easy to report upon. You cannot retrieve records by columns stored in serialised LOB. It's just not suitable for that purpose. But serialised LOBs have this nice feature that once necessity arises, their content can be easily migrated to dedicated column. I.e. let's say that putting employee_id in Order object is becoming a common thing. Now, manager wants to know how do employees perform. Reporting on dynamic column is not possible, but at least, since information was stored in JSON, writing a five-liner that would extract property employee_id and store it into newly created column is a no-brainer. Now, DBA's been saved from heart attack.
However not an one-size-fits-all type of tool, dynamic schema can be a life saver in situations when it is unknown what sort of fields are worth storing into db. It's also perfect for rapid prototyping, where you don't really want to remodel your table columns endlessly and just get the shit running asap. Agile schema modelling can buy you significant amount of time before stabilising your code. Then, once you have pretty good idea on how is this all expected to work, you can start pulling dynamic properties into strict db columns.
10 comments | permalink | trackback | rss
Tak zdechł XHTML i chuj mu na grób
03 Jul 2009
(ten grób pochodzi z piosenki Arkadiusz by Homo Twist)
I badzo dobrze, bo miejsce pięknych standardów jest w muzeach utrzymywanych z pieniędzy podatników zarabiających dzięki standardom, które wprawdzie piękne nie są, ale są za to (by sparafrazować profesora Bartoszewskiego) sympatyczne.
Gęba pełna frazesów
Kiedy byłem pięknym młodzieńcem zwykłem głosić z przekonaniem, że każdy problem da się rozwiązać w standardowy sposób. Jeśli rozwiązanie nie posiada szablonu rozwiązania to należy go stworzyć i ustandaryzować, bo przecież na wszystko da się znaleźć wystarczająco obszerny algorytm czy format. Ponadto dobre rozwiązanie powinno cechować się uniwersalnością, zatem wszystkie pośledniejsze rozwiązania powinny zostać uogólnione za pomocą dodatkowej warstwy abstrakcji. I te pe, i te de, kosmiczna harmonia i zen.
Szybki reality check
Teraz pobawimy się w grę fabularną i ja będę twoim mistrzem gry. Możesz sobie stworzyć avatara, ale nie musisz, bo wynik rozgrywki mogę ci zdradzić już teraz – będąc wielbicielem XHTMLa wielkiego sukcesu nie odniesiesz. I nie myśl, że poniższa gra nie ma wiele wspólnego z developersko-biznesową rzeczywistością. Ma całkiem sporo – straciłem już dawno nadzieję, że życie zacznie mnie nagle rozpieszczać bardziej sensownymi wyborami.
Dołączasz do zespołu tworzącego aplikację WWW. Jest już napisane trochę kodu, który raczej słabo się waliduje. Poziom wiedzy pozostałych członków zespołu jest zróżnicowany: kilku gości świeżo po studiach, jeden nadęty mastahaka, dwójka całkiem sensownych develi. Co robisz?
Zaczynasz tworzyć brakującą funkcjonalność pozostawiając kod niezgodnym z żadnych ze standardów byle tylko działał w FF i IE
Przepisujesz aplikacją na zgodną z XHTML
42
Odpowiedzią na większość problemów, z którymi miałem szansę się zmierzyć była dziwna mieszanka solidnej inżynierii, doświadczenia, dystansu do technologii, szczęścia, paniki, braku czasu oraz jasna konieczność indywidualnego podejścia do zagadnienia. Wszelkie automaty, uniwersalne formaty i metasilniki okazywały się niewłaściwe, w najgorszym przypadku uniemożliwiały wykonanie zadania. Architektoniczni astronauci budują piękne cacka, których nikt nie potrafi poprawnie używać. Dokładnie takie samo zdanie mam o XHTMLu. Jest to przeteoretyzowany gniot, produkt myśli obsesyjnego purysty, który w zamierzeniu ma łączyć cechy XMLa i HTMLa. Byłoby fantastycznie gdyby łączył ich zalety, ale niestety łączy również ich wady. I jest to bardzo zła wiadomość, bo o ile zalety obu formatów czasami bywają przydatne, to ich połączone wady zawsze stanowią przeszkodę. Gdyby było inaczej to wszyscy z uśmiechem na twarzy używalibyśmy takich pereł architektury jak XHTML, SOAP czy CORBA, tym czasem wszystkie wymienione chętnie widziałbym w tym samym miejscu, w którym obecnie znajduje się Betamax.
31 comments | permalink | trackback | rss
PHP+SOAP+WSSE
24 Jun 2009
Pick any two and your life will remain nice and easy. Pick all and you will want to hurt yourself with random object in reach. PHP native class for handling SOAP calls can't do WS-Security. Here's how one can work around this problem.
class WSSESoapClient extends SoapClient {
protected $wsseUser;
protected $wssePassword;
public function setWSSECredentials($user, $password) {
$this->wsseUser = $user;
$this->wssePassword = $password;
}
public function __doRequest($request, $location, $action, $version) {
if (!$this->wsseUser or !$this->wssePassword) {
return parent::__doRequest($request, $location, $action, $version);
}
// get SOAP message into DOM
$dom = new DOMDocument();
$dom->loadXML($request);
$xp = new DOMXPath($dom);
$xp->registerNamespace('SOAP-ENV', 'http://schemas.xmlsoap.org/soap/envelope/');
// search for SOAP header, create one if not found
$header = $xp->query('/SOAP-ENV:Envelope/SOAP-ENV:Header')->item(0);
if (!$header) {
$header = $dom->createElementNS('http://schemas.xmlsoap.org/soap/envelope/', 'SOAP-ENV:Header');
$envelope = $xp->query('/SOAP-ENV:Envelope')->item(0);
$envelope->insertBefore($header, $xp->query('/SOAP-ENV:Envelope/SOAP-ENV:Body')->item(0));
}
// add WSSE header
$usernameToken = $dom->createElementNS('http://schemas.xmlsoap.org/ws/2002/07/secext', 'wsse:UsernameToken');
$username = $dom->createElementNS('http://schemas.xmlsoap.org/ws/2002/07/secext', 'wsse:Username', $this->wsseUser);
$password = $dom->createElementNS('http://schemas.xmlsoap.org/ws/2002/07/secext', 'wsse:Password', $this->wssePassword);
$usernameToken->appendChild($username);
$usernameToken->appendChild($password);
$header->appendChild($usernameToken);
// perform SOAP call
$request = $dom->saveXML();
return parent::__doRequest($request, $location, $action, $version);
}
} // class WSSESoapClient
Use method setWSSECredentials() to enclose WS-Security header in SOAP message.
Code above is a modification and extension of reply to Q#953639 on StackOverflow.com
5 comments | permalink | trackback | rss
Object oriented Web-UI in PHP
28 Feb 2009
Are you familiar with one of these fancy rich-client GUI toolkits? GTK+, Qt, SWT, you name it. They provide nice bridge between what user and programmer see. One would notice that HTML is itself UI platform with all this links, form controls, and JavaScript events. I can't disagree on that, but there's definitely more that could be done to push it more toward high level API rather than low level mark-up to make writing applications easier.
The idea I'm going to present is founded on the elegance and object orientation of SWT I've had chance to work with ages ago. The main goal is to create convenient API to avoid manual HTML/JavaScript coding.
Start holistic
Instead of specing out list of requirements, I'll just write piece of code I'd love to work with – my idealistic way of doing things. This is somewhat holistic approach that may be more appropriate for religion rather than software engineering, but down the line we will get to details.
$login = new Input();
$login->setValue(@$_POST['login']);
$passwd = new PasswordInput();
$button = new SubmitButton();
$form = new Form();
$form->template = 'login-form.tpl';
$form->target = ...some-url...;
$form->add($login)->add($passwd)->add($button);
print($form);
Very basic Web-UI API example
So far so good. We have fairly understandable object oriented API, now it is time to define events. In SWT each listener requires action adapter. Such adapter is most often defined as anonymous subclassed object. Since in web environment most actions is basically described by URL, we don't need action adapter. At least not just yet.
$forgot = new Button();
$forgot->text = 'Forgot my password';
$forgot->addListener(new ClickListener(...some-url...));
$form->add($forgot);
Listener demo
Now, our fancy OO API supports handling events. onclick in this case, but virtually any other is possible. Now what about reading form fields? After user had typed in her name and clicked Forgot button the application should read that name and pass it in URL. Obviously you can't do it on server side, or can you?
$forgot->addListener(
new ClickListener(
...some-url... . '?login=' . $login->valueReader()
)
);
Value reader can be used to read form fields on client-side.
Now what the heck is valueReader()? Think of it as of proxy object for querying form fields on client side. In reality it's not an object (although it could be one implementing __toString() method) it's just a piece of inline JavaScript that gets DOM element by id and returns value property. Since ClickListener generates onclick string it plays nicely with value reader.
So yeah, get by id. But what is the id? I don't know. It should be automatically generated, shouldn't it? Yes, that would help, however I'd rather generate it automaticaly only when not passed to view constructor. This way you can specify id by hand if you need to. But this id thing is a minor problem.
Now consider signup form with more events, listeners, and actions madness. Do you know this nice feature that as you type your login name it checks on the fly if it's available? Surely, we c4n haz t4ht too!
$login = new Input(); $login->addListener( new StopTypingListener( ...some-url... . '?login=' . $login->valueReader() ) );
StopTypingListener is (allegedly) listener that triggers action only when user has stopped typing for certain amount of time. In this example it fires event for checking whether username is free or taken. But wait a minute, you can't just reload whole page when user is half way through the form! Obviously, this needs to be Ajax call and here's where you actually do need action adapter.
$login->addListener(
new StopTypingListener(
new Action(
...some-url... . '?login=' . $login->valueReader(),
...callback-name...
)
)
);
Ajax action adapter example
Tada. Done Ajax way. Now, it's very likely you have your head full of question marks. How is this all implemented?
And now something for reductionists
It is important to state that in above examples I rely heavily on PHP's ability to cast objects to strings. Doing so with __toString() method frees your code from litter otherwise caused by manual casting. Let's just track which object returns what when cast to string.
- View. Returns HTML based on template file. If other views are embeded in that template, they get serialised first and then used in said template. This can trigger a nice cascade when dealing with complex views, listeners, and action adapters. The best thing is that you don't have to do anything to manage this cascade.
- Listener. Returns relevant JavaScript event depending on listener type. For
ClickListenerit'sonclick="...". Body of that event is particularly interesting as it depends on parameter passed to listener. If pure URL is passed, some form oflocation.href = 'url'will be generated. IfActionobject is passed... - Action. Returns JavaScript code performing Ajax call to specified location. (In this example callback function must be passed, but in near future I'm going to present technique I call Ajax Envelope that allows server to define callback after the call has been made and defer its assignment to post-response time).
- Value reader. Returns JavaScript code for reading form field on client side. Can be object, but most often it is just too simple to make it one.
Fear, Uncertainty, and Doubt
Developers, mostly those of little exposure to real life in its whole brutality, tend to raise alarm that having such heavywight API is, uhm, heavy. I have only one answer to that: Like I care! Iron is cheap, programmers' time is not (sorry guys, this reads in Polish). So go and get more memory for your box, or don't and waste more time tinkering with low lever/lightweight API you like more.
So where can I get this from?
Recent activity on Stackoverflow.com brought back my faith in Tigermouse framework I started years ago. Just play with it for a while, see examples, download, have fun, delete or take over further development of the framework as I have no time to do it any more. Oh, please use SVN version of it, as the last official release is terribly outdated.
3 comments | permalink | trackback | rss
Just before you lanuch a PHP application
04 Feb 2009
Applications are buggy. Fact. To take professor Dijkstra's advice I won't call them bugs. I call them errors. Me, you, and other folks do err and these errors do sit in the software quietly ticking to go off when the circumstances arise. Improving software build process so the software is less fscked up does not come for free. In fact it's quite expensive, hence the popularity of Beta sign. Public beta phase is nothing more than employing consumers to do some free error hunting for you. The problem is however, that your targeted audience may just not bother doing your job.
Below you will find few tips I found in my last project – Blastbeat.tv.
Forget about users spontaneously reporting problems
Users do use beta software, but they mostly expect it to be of final version quality. Don't even bother placing "Report problem" link as floating-point number only slightly less than 100 per cent of users will actually use it. And even if somehow 50% of them did it's still merely half of all problems that go under the radar – not too good.
What you should do instead is to develop automated reporting utility. With PHP it is super easy. First, wrap whole application with try…catch statement. Once otherwise unhandled exception is thrown it gets sent via email. This way you get error reported immediately with complete stack trace included. No other user would give you that many details in their report.
Don't forget to redirect user to a friendly page apologizing for technical difficulties with link to 1. previous page, 2. home page. I can't stress enough importance of such links (I should probably blog about it some other day).
Other cheap trick is to redirect PHP errors output to a file. This way you catch all fatal errors, warnings and notices that are not normally reported via exceptions. Put these lines in .htaccess:
php_value error_log log/errors.txt
RewriteEngine On
RewriteRule log/* index.php [F]
Be sure to protect your logs with HTTP 403 status code.
I assume you are quite sane and write your programs with error_reporting set to E_ALL or E_ALL | E_STRICT.
"We are sorry, we were unable to recreate your problem"
Just before you go live make sure you can quickly deploy exact copy of live system on any other machine to recreate problems that occur on production box only. This may seem to you as unnecessary effort, but soon you will find it time-saver whenever new developer joins the team. My additional advice is to keep development environment as heterogeneous as possible. This will surface potential problems sooner than in one-OS, one-DB, one-PHP environment. It is also a very good idea to maintain test installation with db synchronized over night. Having exact copy of your live system will help you pin down errors and save your customer from hearing from you the above quotation.
Care about user experience yourself
As I mentioned earlier users don't bother reporting problems. This means that they also won't bother reporting their not-so-great experience with the site. Experience is something more subtle and elusive, and is not as easy to express as working/not-working situation. You will have to care about it before users turn away.
The most technical aspect of user experience is smoothness of navigation. Page load time is something you can measure using external tools like YSlow or internally just by comparing request and response time. You should also log slow database queries and use indices where appropriate. For MySQL just edit /etc/my.cnf file and add:
log-slow-queries=/var/log/mysql-slow-queries.log long_query_time=1
Read your reports
Now you can focus on reading reports coming to your mailbox. Some of them will be pretty obvious and easy to fix, some can be caused by session expiry, some other by outdated web browsers. The ones caused by web crawlers are particularly interesting as they point to an action that should be probably available only for logged user. Fix them one by one and you'll do great. I managed to take amount of emails from ca. 70 per day down to zero in one man-month. What's really interesting is that I was as same confident that software is relatively bug-free before launch as I am now. The difference is that now I can prove it really is.
11 comments | permalink | trackback | rss
Persistent Objects Metadata
26 Jan 2009
If I was to employ some handy analogy I would say that metadata is to persistent object what interfaces are to object oriented programming. More or less. Persistent Objects Metadata lets you spread common functionality (as described by interface) across different schemas (classes). There is a variety of scenarios where you would like to use metadata, hence I tend to treat this technique as design pattern. (It is different one than Metadata Mapping by Martin Fowler). Areas in which I successfully used this pattern are: searching across several incompatible table schemas, logging access to various objects, building permission system for arbitrary objects. So you can see that this technique brings a layer of common functionality on top of set of incompatible types.
Before we begin there are prerequisites, as there is huge chasm between object-oriented and relational world. And a bridge is needed to map objects to records and the way around. It is important to stress that Persistence Metadata builds relations between objects without using standard relational integrity checks, therefore you cannot rely on restricting/cascading any more. Here is the core interface for dealing with metadata-attached objects:
interface IDistinctive {
public function getDistinctiveId();
public static function instantiate($id);
}
Class must implement this essential interface to use Persistence Metadata technique
Example scenario
One practical example I can show is how to implement logging system that will work for incompatible objects. Imagine set of classes representing persistent objects like Video, Photo, and Song. Assume that db schema is utterly incompatible between each of the three. Also assume that two common operations are required: 1. log plays or views, 2. increase plays/views count. Additional db table is needed for storing metadata information only.
CREATE TABLE videos (
video_id SERIAL PRIMARY KEY,
short_desc TEXT,
long_desc TEXT,
file TEXT,
views_count INT
);
CREATE TABLE photos (
id SERIAL PRIMARY KEY,
name TEXT,
tags TEXT,
album_id INT,
views INT
);
CREATE TABLE songs (
sid SERIAL PRIMARY KEY,
title TEXT,
plays_count INT
);
CREATE TABLE access_log (
distinctive_id INT,
class_name TEXT,
accessed_on DATETIME DEFAULT current_date,
ip_addr VARCHAR(15)
);
Three incompatible table schemas and metadata storage table (in bold).
Not-null constraints and integrity checks skipped for better clarity.
Examining the last table might give you an idea how metadata records are bound with objects. It is by storing class name and distinctive identifier in database (think of it as of a composite pointer to object), so you can store/retrieve persistent object and its metadata independently. It is done via interface previously defined. See example implementation for video class.
class Video extends SomeFancyORMEntity implements IDistinctive {
public function getDistinctiveId() {
return $this->video_id;
}
public static function instantiate($id) {
return Video::findById($id);
}
}
And now logger implementation. All required information is provided by IDistinctive interface and handy get_class function. See this pseudocode example:
class Logger {
public function getLastAccessTime(IDistinctive $o, $ip) {
$cn = get_class(o);
$id = $o->getDistinctiveId();
SELECT max(accessed_on) AS last_access FROM access_log
WHERE class_name = $cn AND distinctive_id = $id AND ip_addr = $ip
return last_access
}
public function log(IDistinctive $o, $ip) {
$cn = get_class(o);
$id = $o->getDistinctiveId();
INSERT INTO access_log (class_name, distinctive_id, ip_addr)
VALUES ($cn, $id, $ip)
}
}
Adding metadata to incompatible objects is now possible thanks to IDistinctive interface.
By providing class names and unique identifiers via IDistinctive interface it is now possible to add metadata for persistent objects of different classes and db schemas. In fact metadata can be stored in different database or server. Or, if you like technology at its extreme, persistent objects could be kept in XML. Not bad, eh?
Having working logger with ability to check last access we can easily detect multiple hits from the same IP and decide whether to increase views/plays count or not. (Just to mention: it is not the best idea to rely solely on IP address to detect false hits).
More goodies
Static log seems to be an append-only data store, but having historical record of all views/plays you can easily report, say last 10 multimedia files. Just improve Logger class slightly by adding this method:
public function getLastItems($n) {
foreach
SELECT class_name, distinctive_id FROM access_log
ORDER BY accessed_on DESC
LIMIT $n
$items[] = class_name::instantiate(distinctive_id);
return $items;
}
Disadvantages
Alert reader will immediately spot two problems here. One (minor) is PHP inability to use variable in static call. The other, much more serious one, is sequential instantiation of persistent objects which is terribly slow for large datasets. There are ugly ways to workaround these problems. To squash vars in static calls eval expression can be employed. It looks really ugly but at least does the job:
eval('$o = ' . $className . "::instantiate($distinctiveId)");
return $o;
Workaround for making static calls on parametrised class.
With a little bit of iterative sorcery sequential instantiation can be brought down to number of different classes in result set returned. This would equate to turning this:
SELECT * FROM videos WHERE video_id = 3653; SELECT * FROM videos WHERE video_id = 824; SELECT * FROM videos WHERE video_id = 5004; SELECT * FROM videos WHERE video_id = 19077; SELECT * FROM songs WHERE sid = 1829; SELECT * FROM songs WHERE sid = 5141; SELECT * FROM photos WHERE id = 8923; SELECT * FROM photos WHERE id = 844;
…into that:
SELECT * FROM videos WHERE video_id IN (3653, 824, 5004, 19077); SELECT * FROM songs WHERE sid IN (1829, 5141); SELECT * FROM photos WHERE id IN (8923, 844);
…which is much better. To achieve this your ORM library must support arbitrary conditions, which is more than likely it does. Splitting and grouping identifiers by class names is done easily:
$groups = array();
foreach ($results as $record) {
$cn = $record['class_name'];
$id = $record['distinctive_id'];
array_key_exists($cn, $groups)
? $groups[$cn][] = $id
: $groups[$cn] = array($id);
}
…and the last thing you need to take care is to restore original order in which records were returned, which I spare myself implementing in this place ;-)
Final remarks
As you can observe Persistent Object Metadata is a quick way of delivering common functionality to variety of incompatible persistent objects. Service class (like logger from example above) implements that functionality relying on metadata storage combined with link (composite pointer) to persistent object. All of the applications I mentioned at the beginning (logging, searching, access control) are present in my current project at work and they show their power as system grows and amount AND versatility of data increases. Permission system is particularly interesting for it maps one persistent object to another (source_id, source_class, target_id, target_class, access_type) and same-class looping can represent hierarchical structure (e.g. user 456 reports to user 234). Metadata introduce overhead in performance, but with a little bit of coding skills and common sense it can be overcome.
add coment | permalink | trackback | rss
Annotations based URL mapping in PHP
19 Nov 2008
I have recently played a bit with different URL mapping implementations. Basically, there are two approaches that are the most common. One is to map controller and method names directly from URL using forwardslash delimiter. Another is to provide global configuration file that stores controller mappings. Usually, the first method is used when no map file is present.
Having some time for experiments I created simple URL mapper parsing method annotations as mappings source. I blogged about annotations in PHP good while ago (sorry guys, it's in Polish) and I found this declarative technique powerful, yet elegant as it is not code intrusive at all. What does it look like then? See the following example.
class NewsCtrl extends Ctrl {
/**
* @URL news/?
*/
public function view($id) { ... }
}
Admittedly, this kind of declaration is much cleaner and easier to read than XML one. Also, you see mapped URLs all the time, not only when working on map file. The question mark here is just a placeholder for controller method parameter. Once URL to this method is requested mapper will return associated annotation. Reading doc-comment block is relatively slow in PHP, so simple file based caching is recommended.
Surely, the example above is simplified beyond real life requirements. Decent mapping engine should at least provide support for
- multiple URLs per controller method,
- variable number of parameters,
- reverse mapping.
Multiple URLs/variable args count can be handled just by simply adding more annotations. As a side effect default parameters get supported.
/**
* @URL news/edit/?
* @URL news/add
*/
public function edit($id = null) { ... }
It is pretty common to use default null parameter to indicate that new entry will be created rather than existing one edited.
Reverse mapping is much more tricky. It requires reading cache file and is done in two steps. First, pattern matching is performed to find mapped URL template, i.e. http://mysite.com/news/edit/25 should match pattern news/edit/?. Secondly, arguments must be extracted from URL based on placeholders (question marks), i.e. 25 is the argument to be passed to controller, and not "news" nor "edit" strings.
URL mapper implementation is quite awful for both mapping directions. In forward mapping, annotation extraction code is a mess and lack of native annotations reflection API is to blame. In reverse mapping two step procedure is in large part strings processing, therefore it simply cannot be pretty ;-)
There are some common pitfalls, though. It is easy to build two overlapping/ambiguous URLs. Look at what we've already built in above examples:
/**
* @URL news/?
*/
public function view($id) { ... }
/**
* @URL news/edit/?
* @URL news/add
*/
public function edit($id = null) { ... }
Now, passing URL like http://mysite.com/news/add reverse mapper may return URL template news/? which is perfectly valid as "news" is exact match and "add" matches placeholder sign (?). As a result string parameter "add" will be passed to method view. This clash can be solved by searching for exact match in first place, however it's still not totally safe and will fail badly when you really intended to pass string "add" to method view. I have no answer to this issue at the moment. The only thing I can suggest is to be super cautious while creating mappings.
Another problem is that when using multiple parameters their order must be preserved.
/** @URL move/?/? */
public function move($from, $to) { ... }
The first parameter here will always be tied to $from variable, and you can't really do much about it. Unless you want to extend reverse mapper so it can parse some kind of ?1, ?2, etc. which is quite an overkill if you ask me.
I deliberately refrained from publishing my mapper implementation, as it is thightly coupled with dispatcher and will differ significantly for each framework. And as I use custom framework it would probably be useless for you anyway. Instead, you will get implementation of annotation parser.
function getAnnotatedURL($class, $method, $argsCount) {
$r = new ReflectionMethod($class, $method);
$doc = $r->getDocComment();
foreach (explode("\n", $doc) as $ln) {
$c = explode(' ', trim($ln, " /*\n\t"));
if (count($c) != 2 or $c[0] != '@URL') {
// process only @URL annotations with 1 parameter
continue;
}
if (substr_count($c[1], '?') == $argsCount) {
$url = $c[1];
break;
}
}
return $url ? $url : null;
}
Pass class and method name, and parameters count and you'll be good to go.
3 comments | permalink | trackback | rss
On-demand CSS/Javascript loading
18 Jun 2008
The web prefers fewer larger files than lots of small ones. Fact. Overall performance is determined by two factors: throughput and latency. In these days throughput seems to be less and less a problem. Optical fibrers, gigabit switches, multicore CPUs, vast RAM and RAIDed storage space make throughput problem passing away. Latency on the other hand is not decreasing. It is the short period of time after request is sent and nothing happens. It is a milisecond value, but when interdependent requests cascade, total latency grows to a significant value. There are ways to work around it. Neither of them is perfect, but some are clearly better than others.
Keep files big. The problem is, that although large files are effective to transfer over network, they are ineffective to work on them. Hell, having one, big, solid CSS file can be a developer's nightmare. I have worked with stylesheets crafted by 3 mindcracked Java developers with tiny knowledge on styles, cool guys on the other hand. Many sections were written numerous times, definitions were overlaping and tracking down inheritance and cascading issues was a tedious job. Beyond some point this is no longer a solution when project grows.
Use linker. Before uploading to the server, files can be merged into single one. Additionally, some text-level optimisation can be done, such as \r removal or comments stripping. The good side of using linker is basically, that it does its job. The downside is that it sucks. You have to re-run linker every single time you modify Javascript or stylesheet. Another drawback is that when Firebug says "error in line 825" there is no way to map it back to before-linking state.
Use automated on-demand loading. This is my favourite. The idea is to keep CSS and Js in small files and transfer them in single request. To download all required files in single http connection, a proper request is needed. This is relatively easy part and can be done straight away:
<link rel="stylesheet" type="text/css" href="css.php?f=style1.css,style2.css,style3.css"/> <script type="text/javascript" src="js.php?f=script1.js,script2.js,script3.js"></script>
You can probably imagine the implementation for css.php and js.php, just remember to add some important headers:
Content-type: text/javascript (or text/css)
Cache-control: public, max-age=3600
Pragma: public
Also, sanitize inclusion path and/or verify MIME type for requested files - you definitely do not want to publish your db.ini settings.
As I am well known to be a OOP junkie, the solution uses nice wrapper for Smarty template engine and some PHP magic methods starting with double underscore sign.
class View {
protected $properties = array();
public $templateFile;
public function __set($p, $v) {
$this->properties[$p] = $v;
}
public function __get($p) {
return $this->properties[$p];
}
protected function getSmarty() {
$t = new Smarty(); // some further Smarty configuration is needed here
foreach ($this->properties as $p => $v) {
$t->assign($p, $v);
}
return $t;
}
public function __toString() {
$t = $this->getSmarty();
return $t->fetch($this->templateFile);
}
}
What you see above is a very basic view implementation. Whatever property you set in this object, it will be reflected as template variable. When casting object to string __toString method will be called providing relevant string representation for that object (use PHP 5.2+ to avoid casting problems with . operator). See the example myTemplate.tpl and its use.
<div>
Hello {$subject}! What a {$adjective} day we have!
</div>
$v = new View(); $v->templateFile = 'myTemplate.tpl'; $v->subject = 'Random User'; $v->adjective = 'beautiful'; echo $v;
Ok, this is rather obvious that we'll get Hello Random User! What a beautiful day we have! as a result. But the best is about to come. Views implemented this way can be easily nested. You can have index.tpl template that will become a backbone of your site. There is also initial support for CSS and Javascript autoinclusion in single request:
<html>
<head>
<title>{$title}</title>
<link rel="stylesheet" type="text/css" href="css.php?f={$css}"/>
<script type="text/javascript" src="js.php?f={$js}"></script>
</head>
<body>
{$content}
</body>
</html>
$page = new View(); $page->templateFile = 'index.tpl'; $page->title = 'My lame page'; $v = new View(); $v->templateFile = 'myTemplate.tpl'; $v->subject = 'Random User'; $v->adjective = 'beautiful'; $page->content = $v; echo $page;
This snippet is probably rendered in a browser the same way as the previous one, but at least we have proper HTML document (well, actually DOCTYPE is missing here ;-). For CSS/Js autoinclusion we need to add some index page specific logic. It is convenient to derive Index class from View.
class Index extends View {
public $templateFile = 'index.tpl';
public static $css = array();
public static $js = array();
public static function registerStyle($f) {
in_array($f, self::$css) or self::$css[] = $f;
}
public static function registerScript($f) {
in_array($f, self::$js) or self::$js[] = $f;
}
protected function getSmarty() {
$t = parent::getSmarty();
$t->assign('css', implode(',', self::$css));
$t->assign('js', implode(',', self::$js));
return $t;
}
}
...and a specialised view that will require CSS and Javascript inclusion in constructor:
class MyStyledView extends View {
public $templateFile = 'myTemplate.tpl';
public function __construct() {
Index::registerStyle('myStyle1.css');
Index::registerStyle('myStyle2.css');
Index::registerScript('myScript.js');
}
}
Let's also rewrite the second example to make use of our new classes, Index and MyStyledView:
$page = new Index(); $page->title = 'My styled page'; $v = new MyStyledView(); $v->subject = 'Random User'; $v->adjective = 'beautiful'; $page->content = $v; echo $page;
The result in pure HTML should be like (notice the lines in bold):
<html> <head> <title>My styled page</title> <link rel="stylesheet" type="text/css" href="css.php?f=myStyle1.css,myStyle2.css"/> <script type="text/javascript" src="js.php?f=myScript.js"></script> </head> <body> <div> Hello Random User! What a beautiful day we have! </div> </body> </html>
Look at what you gain and what you loose with the on-demand approach presented.
- Single request style/script inclusion
- Separate files on server side
- Automated registration of required styles/scripts
Finally, there is also a simple solution for "error in line 825" problem I mentioned before. To map back result file line number to source file and its respective line number, just add comments before each file is merged and put global and local line number before every line in that file. Such comments are activated only by additional GET parameter and normally are not sent to client.
On the down side there is a little overhead when loading script or style URL for the first time - partially duplicated content is not cached yet. The other problem is that this technique does not work with Ajax, so you will have to write different loader for Ajax calls. However still, it is a good solution if you like to keep CSS and Javascript separated into logical blocks.
4 comments | permalink | trackback | rss
Uniwersalne wyszukiwanie - PHP
10 Jun 2008
Przykład wyszukiwarki wałkowałem już kiedyś na tym blogu przy okazji rantu o Traitsach w PHP. Nie był on jednak rozpracowany zbyt dokładnie, w końcu służył tylko zilustrowaniu pewnego problemu.
Wyszukiwanie danych rozumiane jako dziura na tekst i przycisk "Znajdź" wydaje się być zagadnieniem dość prostym dopóki wiemy wśród czego szukamy (np. poprzez proste SELECT * FROM cars WHERE make LIKE ...). Szukanie bez określonego kontekstu w sposób jak powyżej wydaje się być jest zadaniem beznadziejnym...
W poszukiwaniu prostego i skutecznego indeksowania w aplikacji PHP doszedłem ostatnio do zadawalającego rozwiązania. A ponieważ uwielbiam podniecać się technikami obiektowymi - rozwiązanie to ląduje na jogu. Posługuję się w nim techniką Dependency Injection do indeksowania treści w locie przy operacji zapisu przez DAO. Obowiązkowo występować w nim musi implementacja wyszukiwarki (najprostsza możliwa, wykorzystująca PDO):
CREATE TABLE search (
class TEXT NOT NULL,
key INT NOT NULL,
content TEXT
);
CREATE INDEX search_content_idx ON search (content);
Schemat tabeli przechowującej poindeksowaną treść.
class SearchEngine {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function update($class, $id, $text) {
$this->pdo->exec("DELETE FROM search WHERE class = '$class' AND key = '$id'");
$content = $this->pdo->quote($content);
$this->pdo->exec("INSERT INTO search (class, key, content) VALUES ('$class', '$id', '$content')");
}
public function find($text) {
$condition = array('TRUE');
foreach (explode(' ', $text) as $t) {
$t = $this->pdo->quote($t);
$condition[] = "content LIKE '%t%';
}
$condition = implode(' AND ', $condition);
$sql = "
SELECT class, key FROM search
WHERE $condition;
";
$stmt = $pdo->query($sql);
return $stmt;
}
}
Przykład implementacji wyszukiwarki. Algorytm szeregowania znalezionych obiektów realizujemy w klauzyli ORDER BY (pominięta dla czytelności).
oraz interfejs ISearchable:
interface ISearchable {
public function updateSearchEngine(SearchEngine $se);
}
Metoda update przyjmuje nazwę klasy, unikalny identyfikator obiektu (najczęściej klucz główny) oraz treść do zaindeksowania. Metoda find zwraca pary (nazwa_klasy, klucz_główny), w których szukana fraza została odnaleziona.
Załóżmy, że naszym przykładowym obiektem dziedziny modelu jest wpis na blogu reprezentowany przez klasę BlogPost. Obiekty tej klasy potrafią uaktualnić informację o sobie w wyszukiwarce dzięki implementacji interfejsu ISearchable, konkretnie indeksowaniu podlega tytuł, treść oraz tagi dotyczące wpisu.
class BlogPost implements ISearchable {
public $id, $title, $body, $tags;
// reszta implementacji klasy
public function updateSearchEngine(SearchEngine $se) {
$se->update(get_class($this), $this->id,
$this->title . ' ' . $this->body . ' ' . $this->tags);
}
}
Ponieważ wywoływanie metody updateSearchEngine za każdym razem, gdy zawartość obiektu się zmienia byłoby niepraktyczne, można zadanie powierzyć obiektowi DAO, który rozpozna instancję ISearchable i wykona na niej tą metodę. Do tego potrzebna jest drobna modyfikacja klasy DAO (zakładam, że jakąś już masz):
class DAO {
private $searchEngine;
public function save($o) {
... // stuff DAO normally does
if ($o instanceof ISearchable) {
$o->update($this->searchEngine);
}
}
}
Od teraz, za każdym razem, gdy będziesz utrwalać obiekt za pomocą DAO, będzie on aktualizował wyszukiwarkę informacjami dostarczonymi przez obiekt dziedziny.
Gdy już wszystko działa jak należy, możemy przystąpić do zrozumienia tej kooperacji :-)
Po pierwsze, tworzony jest centralny rejestr poindeksowanej treści. Po drugie, to obiekt dziedziny wie jaką treść (tj. które pola) powinien zaindeksować. Po trzecie, indeksowaniem i aktualizacją zajmuje się ten, kto ma władzę nad trwałością, czyli DAO.
Dążąc do uniwersalności tego rozwiązania, co obiecałem w tytule, wprowadźmy kolejną klasę, której obiekty będą indeksowane. W przypadku bloga dość naturalnie nasuwa się na myśl BlogPostComment.
class BlogPostComment implements ISearchable {
public $id, $blogPostId, $body, $author;
public function updateSearchEngine(SearchEngine $se) {
$se->update(get_class($this), $this->id, $this->body . ' ' . $this->author);
}
}
Nie zmieniając nic w implementacji wyszukiwarki możemy w ten sposób dodawać funkcjonalność wyszukiwania do dowolnego obiektu dziedziny zagadnienia.
Final remarks.
- Implementacja wspiera jedynie indeksowanie treści oraz wyszukiwanie obiektów zawierających ją. Nie wspiera natomiast prezentowania tej treści. Będziesz do tego potrzebował jakiegoś mechanizmu, który na podstawie zwróconej pary (nazwa_klasy, klucz_główny) powoła obiekt (lub wydobędzie treść bezpośrednio z bazy) i wyświetli odpowiednie pola w pożądanym formacie.
-
Implementacja w oparciu o formalny interfejs nie jest wcale obowiązkowa. Można to zrobić w szpanerskim stylu używając adnotacji - przykład używania AOP opisywałem przy okazji wywodu nad prawami dostępu. Adnotacja
@Searchablebędzie wyglądać niezwykle szykownie przed definicją klasy obiektu dziedziny :-) - Opisana implementacja samej wyszukiwarki nie jest w żaden sposób kompletna, czy optymalna. Jest ona uproszczona do granic możliwości, by przykład nadawał się do analizy. Algorytm szeregowania treści należy umieścić w klauzuli ORDER BY. Warto również zwrócić uwagę na redundancję powstającą w wyniku indeksowania treści. Nie jest ona niczym niewłaściwym dopóki nie modyfikujemy tabel obiektów dziedziny ręcznie, lub w jakikolwiek inny sposób z pominięciem DAO.
Miłej zabawy!
3 comments | permalink | trackback | rss
Prawa dostępu oparte o adnotacje - PHP
24 Apr 2008
Adnotacje - to ciekawe podejście do zarządzania metadanymi z poziomu kodu źródłowego. Adnotacje pozwalają na opisanie klas, metod i własności dodatkowymi informacjami, które mogą być wykorzystane w dalszym przetwarzaniu. Najprostszym możliwym przykładem jest rozszerzenie obiektu wartości zwracanego przez DAO o informacje dotyczące schematu tabeli (m.in. typ pola, dozwolona wartość null, klucze obce, itp.).
class Account extends ValueObject {
/**
* @Column int
* @NotNull
*/
public $id;
/**
* @Column varchar
* @NotNull
*/
public $login;
...
...
}
Wyposażając obiekt DAO w zdolność odczytywania adnotacji możemy zautomatyzować elementarną walidację poszczególnych pól obiektu wartości, a nawet tworzyć brakujące tabele i pola w locie. Ponieważ ostatnio dużo zajmowałem się systemami praw dostępu w aplikacjach webowych, przyszła mi do głowy możliwość ożenienia adnotacji z usługą kontroli dostępu do metod kontrolerów. Zerknijmy na przykładowy kontroler i adnotacje @Auth przy jego metodach:
class Users extends Controller {
/** @Auth users-browse */
public function index() { ... }
/** @Auth users-add */
public function addUser() { ... }
/** @Auth users-delete */
public function deleteUser() { ...}
}
Przykład przedstawia klasę kontrolera z metodami zaszeregowanymi do różnych obszarów dostępu (users-browse, users-add, users-delete). Taki sposób oznaczania obszarów aplikacji ma szereg zalet. Pierwszą z nich jest oddzielenie kodu określającego prawa dostępu od kodu logiki biznesowej. Drugą jest duża czytelność i łatwość wprowadzania zmian. Kolejna zaleta to możliwość automatyzacji procesu zbierania informacji o obszarach aplikacji - wystarczy prosty grep by uzyskać listę takich obszarów.
Pora teraz przygotować usługę weryfikacji dostępu. Na potrzeby przykładu ograniczę się do absolutnego minimum. Przede wszystkim schemat tabeli, w której zapisane będą prawa wygląda następująco:
CREATE TABLE app_access ( user_id INT, role_id INT, area VARCHAR(200) );
Usługa sprawdzania dostępu ma m.in. metodę isUserAuthorized:
class AppAccessService {
public static function isUserAuthorized(User $user, $area) {
if ($user->isAdmin()) {
return true;
}
$userId = $user->getId();
$roleId = $user->getRoleId();
$sql = "
SELECT * FROM app_access
WHERE (user_id = $userId OR role_id = $roleId)
AND area = '$area'
";
// reszta obsługi bazy danych...
}
}
Mamy już informację o obszarach aplikacji oraz usługę sprawdzania dostępu. Brakuje tylko automatyki, która będzie dokonywała tego sprawdzenia przed każdym wywołaniem metody kontrolera. Jeśli Twój framework nie wspiera filtrów wejścia/wyjścia to pozostaje Ci implementacja w konstruktorze kontrolera i odgadywanie metody na podstawie URLa. Jeżeli wspiera i pozwala dowiedzieć się jaki kontroler i jaka metoda zostaną wywołane zanim się to stanie, to fantastycznie. Na potrzeby przykładu zakładam, że zmienne $className i $methodName zawierają nazwę klasy i metody kontrolera.
$r = new ReflectionMethod($className, $methodName);
$doc = $r->getDocComment();
foreach (explode("\n", $doc) as $ln) {
$ln = trim($ln, " /*\n\t");
$c = explode(' ', $ln);
if ($c[0] == '@Auth') {
if (!AppAccessService::isUserAuthorized($user, $c[1])) {
throw new AppAccessException("Access to {$c[1]} denied");
}
}
}
Jak widać implementacja jest króciutka, ale niestety mało czytelna. To wina braku natywnej obsługi introspekcji dla adnotacji (Annotation Reflection API ma się pojawić w PHP 5.3). Taki lub podobny kod powinien znaleść się w jednym z filtrów, natomiast cała aplikacja powinna zostać ujęta w blok try-catch chwytający wyjątki typu AppAccessException. W razie wystąpienia wyjątku należy przekierować użytkownika na stronę z informacją o braku dostępu i innymi groźbami.
7 comments | permalink | trackback | rss
Równomierne rozłożenie obrazków w dokumencie
18 Apr 2008
TinyMCE fajnym edytorem jest. Problem leży jednak w tym, że czasem zbyt fajnym (bo nikt nie odważy się przecież powiedzieć klientowi, że program jest fajny, tylko jego użytkownicy są głupi ;-). Przycisk dodawania obrazków do dokumentów przyprawia o ból zębów każdego, kto ma jakiekolwiek poczucie estetyki (innej niż ta z Cepelii). Userzy wrzucają je całkowicie bezmyślnie, bez formatowania i najczęściej w absurdalnych rozdzielczościach i w ogóle robią bydło.
Rozwiązanie jest proste - zabierz userom przycisk obrazka, a w zamian daj własny widget do zarządzania fotografiami i rozkładaj je w dokumencie automatycznie. Pisanie uploadera, thumbnailera i obsługi listy to banał i był wałkowany milion razy, bardziej zagadkowe jest natomiast równomierne rozłożenie obrazków w dokumencie.
Pierwotnie próbowałem rozwiązać ten problem po stronie serwera - zliczać paragrafy i wklejać tagi IMG. Nie mogąc uzyskać zadawalających wyników, przeniosłem kod do klienta. Oto implementacja w Javascripcie:
spreadImages = function (bodyId, imagesId) {
var bw = document.getElementById(bodyId);
var iw = document.getElementById(imagesId);
var ps = bw.getElementsByTagName('P');
var imgs = iw.getElementsByTagName('IMG');
var n = imgs.length; // ważne by skopiować rozmiar tablicy
var left = true;
var d = ps.length / imgs.length;
for (var i = 0; i < n; ++i) {
var tmp = iw.removeChild(imgs[0]);
var p = ps[Math.round(d * i)];
p.insertBefore(tmp, p.firstChild);
tmp.style.cssFloat = left ? 'left' : 'right';
tmp.style.styleFloat = left ? 'left' : 'right';
left = !left;
}
};
Jak widać zadanie nie okazało się wcale takie trudne. W wywołaniu podajemy dwa parametry. Pierwszy to identyfikator elementu otaczającego dokument, drugi to identyfikator elementu zawierającego obrazki. Na dowód, że kod działa zamieszczam poniżej kilka fotografii z wycieczki na Giants Causeway w Północnej Irlandii, oraz magiczny przycisk.
Po przeładowaniu strony kliknij na obrazek by go usunąć i spróbować ponownie dla innej liczby fotografii.
4 comments | permalink | trackback | rss
Prawa dostępu, the ultimate edition
02 Apr 2008
Prawa dostępu w aplikacjach WWW - dla jednych przekleństwo, dla innych środek nadmiernego wyrazu, za każdym jednak razem całkiem spory kawałek bardzo ważnego kodu. Co ważniejsze, nie kod jest tu najważniejszy, a poprawny projekt oraz łatwość użycia (bo cóż po systemie dostępu kiedy współprogramiści pozostawiają sobie włączenie go na później - czyli nigdy).
Po jednym z bezcelowych i przeraźliwie nudnych spotkań z managementem zaproponowano mi ("ty to zrobisz")
wykonanie systemu praw dostępu ze szczegółowością na poziomie paranoi: prawami obwarowany miał być
absolutnie każdy element aplikacji, od poszczególnych wierszy i tabel, poprzez podstrony, aż do użytkowników
i ról. To wszystko w logice trójstanowej zezwol-zabroń-dziedzicz dla każdego z przedmiotów oraz dodatkowo dla dowolnej
czynności związanej z przedmiotem i oczywiście z relacją wiele-do-wielu na linii użytkownik-rola.
Ponieważ management nie bardzo rozumie konsekwencje swoich wysokopoziomowych decyzji
ani niuanse implementacyjne (to już w ogóle) poprosiłem o rozwiązanie kilku przypadków, które prędzej czy
później pojawią się w codziennym używaniu:
- jak rozwiązać konflikt ról - gdy użytkownik w roli A ma dostęp do zasobu, a rola B dostępu zabrania?
- jak rozwiązać konflikt hierarchii zasobów - gdy dozwolony zasób X jest częścią niedostępnego zasobu Y
(sytuacja częsta przy udostępnianiu zasobów pomiędzy użytkownikami)
oraz coś, co ich ostatecznie położyło:
- Jeff, skoro to Ty będziesz tym zarządzał, to narysuj mi proszę jak chciałbyś to widzieć na ekranie (podły ze mnie chujek, nie?).
Menedżment się zamknął i dla odmiany zaczął słuchać. Użytkownicy-role będą odwzorowane relacją jeden-do-wielu,
prawa do zasobów będą opisane w logice binarnej zgodnie z zasadą "co nie jest dozwolone, jest zabronione". Konflikty hierarchii
zasobów rozwiązywane są w sposób jawny dla programisty (poprzez wyrzucenie wyjątku). Ekrany zarządzania prawami
narysowałem sobie sam.
Muszę powiedzieć, że mimo początkowych obaw, że tak okrojone prawa będą niewystarczające, warunki bojowe pokazały co następuje:
- użytkownicy olewają konfigurację praw dostępu i jadą na defaultach,
- ci, którzy nie olewają, mają na tyle czytelne ekrany, że nie przybiegają z prośbą o pomoc w konfiguracji praw.
W ten sposób management "poniósł ogromny sukces" a ja mam święty spokój.
Dość przynudzania, pora na mięso. Semantycznie, wszystkie zasoby da się zredukować do dwóch typów:
obiekty z unikalnym identyfikatorem oraz klasy i metody kontrolerów. Na potrzeby tych pierwszych wyrzeźbiłem klasę
usługową AuthService, drugie są obsługiwane przez AppAccessService.
Każdy obiekt o unikalnym identyfikatorze, który może podlegać prawom dostępu musi implementować interfejs
IAuthorizable, składający się z dwóch metod: getDistinctiveId zwracającej
identyfikator w przestrzeni nazw takiej, jak nazwa klasy, oraz getAccessModes zwracającej tablicę
dostępnych typów dostępu (np. 'read', 'write', 'delete'). W ogromnej większości przypadków unikalnym identyfikatorem
jest klucz główny odpowiadającego obiektu trwałego. Schemat tabeli do przechowywania praw dostępu jest następujący:
CREATE TABLE access ( user_id INT REFERENCES users (id) ON DELETE CASCADE, role_id INT REFERENCES roles (id) ON DELETE CASCADE, class_name VARCHAR(100), distinctive_id INT NOT NULL, mode VARCHAR(20) NOT NULL ); CREATE INDEX access_user_id_idx ON access (user_id); CREATE INDEX access_role_id_idx ON access (role_id); CREATE INDEX access_subject_idx ON access (class_name, distinctive_id, mode);
Można jako typ pola distinctive_id zastosować również VARCHAR, ale należy się wówczas liczyć
z istotną utratą wydajności bazy.
Implementacja usługi sprawdzania i definiowana dostępu jest dość prosta
(część odpowiedzialna za obsługę bazy danych została pominięta):
class AuthService {
public static function isUserAuthorized(User $user, IAuthorizable $subject, $mode) {
$distinctiveId = $subject->getDistinctiveId();
$className = get_class($subject);
$userId = $user->getId();
$roleId = $user->getRoleId();
$sql = "
SELECT count(*) AS c FROM access
WHERE distinctive_id = $distinctiveId
AND class_name = '$className'
AND (user_id = '$userId' OR role_id = '$roleId')
";
// dalej mało ważne pierdoły związane z obsługą bazy
}
public static function isRoleAuthorized(Role $role, IAuthorizable $subject, $mode) {
// to samo co w isUserAuthorized, tylko bez sprawdzania user_id = '$userId'
}
public static function authorizeUser(User $user, IAuthorizable $subject, $mode) {
$distinctiveId = $subject->getDistinctiveId();
$className = get_class($subject);
$userId = $user->getId();
$sql = "
INSERT INTO access (user_id, class_name, distinctive_id, mode)
VALUES ($userId, '$className', $distinctiveId, '$mode');
";
}
public static function authorizeRole(Role $role, IAuthorizable $subject, $mode) {
// to samo do w authorizeUser, tylko z role_id zamiast user_id
}
}
Dodajemy jeszcze odpowiednie metody do odbierania praw dostępu i klasa jest prawie gotowa. Prawie używalna. Mamy już kompletną implementację bardzo szczegółowych praw dostępu, która jest łatwa w używaniu. Tak łatwa jak:
if (AuthService::isUserAuthorized($user, $resource, 'read')) {
// zapraszamy
} else {
// wypad
}
To, co łatwo przegapić w tym momencie, to słaba wydajność takiego rozwiązania podczas operacji na dużych zestawach danych.
Wyobraź sobie iterowanie przez 1000 obiektów pobranych jednym selektem. W rezultacie otrzymasz 1001 zapytań do bazy -
jedno szybkie, zwracające ogromną ilość danych i 1000 kompletnie nieoptymalnych, zwracających pojedynczą liczbę. Jest i na to sposób:
wystarczy pobrać tylko te wiersze, które mają dostęp w podanym trybie dla konkretnego użytkownika/roli. Zaimplementowałem to za pomocą
delegata praw dostępu używanego przy wybieraniu wierszy z bazy. Obiekt delegata tworzony jest przez klasę AuthService
class AuthService {
...
...
public static function getUserAuthorizationDelegate(User $user, $mode) {
return new UserAuthorizationDelegate($user, $mode);
}
public static function getRoleAuthorizationDelegate(Role $role, $mode) {
return new RoleAuthorizationDelegate($role, $mode);
}
}
Natomiast implementacja obiektu delegata wywodzi się z klasy o abstrakcyjnej metodzie getExpression:
class UserAuthorizationDelegate extends AuthorizationDelegate {
protected $userId;
protected $roleId;
protected $mode;
public function __construct(User $user, $mode) {
$this->userId = $user->getId();
$this->roleId = $user->getRoleId();
$this->mode = $mode;
}
public function getExpression($className) {
$sql = "
SELECT DISTINCT distinctive_id FROM access
WHERE (user_id = '{$this->userId}' OR role_id = '{$this->roleId}')
AND class_name = '$className'
AND mode = '$mode'
";
return $sql; // tym razem zwracamy czysty SQL
}
}
Implementacja delegata dla ról wygląda podobnie. Teraz możemy zająć się dostosowaniem kodu, który wybiera z bazy wiersze, by współpracował z delegatem. Zwyczajowo najczęściej korzystam z kombinacji wzorców DAO i ValueObject, ale po drobnych modyfikacjach możliwe jest skorzystanie ze wzorców ActiveRecord, TableGateway oraz prawdopodobnie innych dziwactw.
class FooDAO {
...
...
public static function getAll(AuthorizationDelegate $delegate = null) {
$condition = $delegate
? 'id IN (' . $delegate->getExpression('Foo') . ')'
: 'TRUE';
$sql = "
SELECT * FROM foos
WHERE $condition
ORDER BY id
";
// obsługa bazy
}
}
Jeśli preferujesz wybranie wszystkich wierszy i oznaczenie ich dostępności w dodatkowej kolumnie, zmodyfikuj zapytanie w ten sposób:
SELECT f.*, $condition AS is_authorized FROM foos AS f ORDER BY id
Na koniec coś, co otwiera oczy niektórych niedowiarków na piękno i siłę technik obiektowych. Wspomniana powyżej klasa AppAccessService
może również zwracać swoich delegatów. Ich konstruktory zapewne będą różne od tych zwracanych przez AuthService ale interfejsy
zostaną zachowane, dzięki czemu możemy ich używać wymiennie i w takim samym kontekście (zarówno delegatów jak i klas usług dostępu).
Co więcej, możemy bez problemu łączyć delegaty w überdelegaty za pomocą wzorca Composite i nadal używać ich w ten sam sposób.
class UberDelegate extends AuthorizationDelegate {
protected $delegates = array();
public function addDelegate(AuthorizationDelegate $delegate) {
$this->delegates[] = $delegate;
}
public function getExpression($className) {
$out = array();
foreach ($this->delegates as $delegate) {
$out[] = $delegate->getExpression($className);
}
return empty($out) ? 'TRUE' : implode(' UNION ', $out);
}
}
W zrozumieniu wszystkiego co napisałem powyżej wydatnie pomoże znajomość UML-a oraz papier i ołówek. Będzie mi miło usłyszeć o wykorzystaniu tego rozwiązania w praktyce, jak również o alternatywnych sposobach implementacji praw dostępu. Acha, jeszcze tylko legal bullshit: kod jest wolną reimplementacją moich pomysłów z pracy z poprawionym nazewnictwem i usuniętymi niepotrzebnymi zależnościami. Używaj do woli w zgodzie z LGPL 2 lub nowszej.
Update 1207701709: Dodałem diagram klas dla lepszej czytelności rozwiązania.
Update 1207781421: Jest i diagram sekwencji w dwóch wydaniach. Pierwszy obrazuje proste sprawdzenie prawa do zasobu (Product), drugi przedstawia użycie delegata praw.
Klient tworzy obiekt zasobu i odpytuje AuthService o prawo dostępu. Usługa pobiera z produktu jego identyfikator (ufając, że ma doczynienia z obiektem implementującym interfejs IAuthorizable). Zapytanie do bazy odpowiada czy user (pominięty w tym diagramie) ma dostęp w żądanym trybie.
Klient pobiera z AuthService delegata właściwego dla podanego użytkownika (pominięty w diagramie). Delegat jest tworzony i przekazywany do statycznego wywołania getAll klasy zasobów. Klasa ta odpytuje odpowieni obiekt DAO i przekazuje mu delegata. Obiekt dostępu do danych (DAO) pobiera z delegata wyrażenie ograniczające SQL, następnie włącza je do swojego SQLa i odpytuje bazę. Na podstawie zwróconych rekordów tworzy kolekcję zasobów, która wraca do klienta.
Przy okazji: Umbrello nie zna diagramu sekwencji, więc na szybko użyłem Boumla. Jeden gorszy od drugiego, ale do szkiców wystarczające.
4 comments | permalink | trackback | rss
Traits w PHP
22 Feb 2008
Kilka dni temu dostałem z PHP Ireland powiadomienie o propozycji włączenia do standardu języka konstrukcji Traits. Traits to mechanizm obchodzący ograniczenia dziedziczenia jednobazowego, umożliwiający zaimportowanie metod do klasy oraz jawne oznaczenie z jakich Traitsów klasa korzysta - jak przy interfejsach. Dla znających UML, Traits to jak Implementation Classes, które są abstrakcyjne tak jak interfejsy, ale posiadają implementację jak zwykłe klasy.
Polecam zapoznać się ze wspomnianym RFC oraz przykładami zanim go obsobaczę. Do czytania marsz.
Na pierwszy rzut oka wydaje się, że Traits to dopust boży dla wszystkich, którzy kiedykolwiek potrzebowali wyposażyć szereg klas we wspólną funkcjonalność. Przyjazna konstrukcja językowa przypominająca nieco CSS również może się podobać. Po chwili zastanowienia przychodzi refleksja, że przecież cały czas radzę sobie z podobnymi problemami bez użycia Traitsów!
Wyobraźmy sobie problem prawie rzeczywisty - potrzeba kompleksowego wyszukiwania po zadanej frazie wśród rozmaitych obiektów trwałych: użytkowników, dokumentów, produktów, itp. Z punktu widzenia projektanta obiektowego, klasa implementująca przede wszystkim trwałość nie jest właściwym miejscem dla systemu szukania. Traits przychodzi z rozwiązaniem: zrób implementację wyszukiwania osobno i włącz ją do klasy. Efekt końcowy jest taki, że mamy implementację szukania w klasie realizującej trwałość, czyli niepoprawnie. Jak zatem zrobić to właściwie?
Można wykorzystując interfejs i kompozycję oraz pewną sprytną technikę programistyczną:
class SearchEngine {
protected $table;
protected $columns;
public function __construct($table, $columns) {
$this->table = $table;
$this->columns = $columns;
}
public search($term) {
$sql = "SELECT * FROM {$this->table} WHERE FALSE";
foreach ($this->columns as $column) {
$sql .= " OR $column ILIKE '%$term%'";
}
// database stuff here
}
}
interface ISearchEngineProvider {
public static function getSearchEngine();
}
class User implements ISearchEngineProvider {
// class implementation here
public static function getSearchEngine() {
return new SearchEngine('users', array('login', 'name'));
}
}
class Product implements ISearchEngineProvider {
// class implementation here
public static function getSearchEngine() {
return new SearchEngine('products_view', array('name', 'category_name', 'make'));
}
}
Widzimy tu czterech uczestników tego teatrzyku (ha, rym!): prosta implementacja wyszukiwania SearchEngine, interfejs dostarczyciela wyszukiwarki ISearchEngineProvider oraz dwie przykładowe klasy użytkownika i produktu.
Klasy User i Product dostarczają informację o implementowanym interfejsie, można więc na nich wywołać statyczną (statyczną dla wygody) metodę getSearchEngine(), która zwróci nam skonfigurowaną wyszukiwarkę. Technika ta nazywa się Dependency Injection i jest niezwykle skuteczna w czynieniu złożonych problemów prostymi - jak w powyższym problemie, gdzie klasy użytkownika i produktu nie mają wspólnego przodka. Element różnicujący zostaje "wstrzyknięty" do kontenera SearchEngine w konstruktorze, ale typ kontenera pozostaje niezmienny.
Możliwa jest dalsza wariacja na temat zwracanego wyniku wyszukiwania. Klasy User i Product mogą zwracać swoje własne wyszukiwarki wyprowadzone z klasy SearchEngine. I tak User::getSearchEngine() może zwracać obiekt klasy UsersSearchEngine, który z kolei zwróci gotowe intancje User zamiast zwyczajnych wierszy z bazy danych, a Product::getSearchEngine() zrobi to analogicznie dla produktów. Typ kontenera nadal pozostaje niezmienny, gdyż zróżnicowane wyszukiwarki są pochodnymi klasy SearchEngine.
Z powyższego rodzi mi się parę luźnych spostrzeżeń:
- problemy rozwiązywane przez Traits da się z łatwością rozwiązać poprzez poprawnie zaprojektowaną strukturę obiektów
- PHP za wszelką cenę chce być bardziej dynamiczny niż Ruby
- dynamic is not the only way to go
- Traits pozwalają kiepskim developerom poczuć się lepiej
- niektórzy developerzy nie odrobili lekcji i gówno wiedzą o interfejsach, kompozycji i delegacji odpowiedzialności do klas usługowych
Dodatkowo gdy czytam o rozwiązywaniu konfliktów w przestrzeni nazw klasy lub Traitsów to wydaje się to dość czytelne, ale gdy wyobrażam sobie kod stworzony w ten sposób przez jakiegoś żółtodzioba to przechodzą mi ciarry.
Wuj Dobra Rada: nie używaj tego gada.
9 comments | permalink | trackback | rss







