Viewed   56 times

How do you organize and manage your helper objects like the database engine, user notification, error handling and so on in a PHP based, object oriented project?

Say I have a large PHP CMS. The CMS is organized in various classes. A few examples:

  • the database object
  • user management
  • an API to create/modify/delete items
  • a messaging object to display messages to the end user
  • a context handler that takes you to the right page
  • a navigation bar class that shows buttons
  • a logging object
  • possibly, custom error handling

etc.

I am dealing with the eternal question, how to best make these objects accessible to each part of the system that needs it.

my first apporach, many was to have a $application global that contained initialized instances of these classes.

global $application;
$application->messageHandler->addMessage("Item successfully inserted");

I then changed over to the Singleton pattern and a factory function:

$mh =&factory("messageHandler");
$mh->addMessage("Item successfully inserted");

but I'm not happy with that either. Unit tests and encapsulation become more and more important to me, and in my understanding the logic behind globals/singletons destroys the basic idea of OOP.

Then there is of course the possibility of giving each object a number of pointers to the helper objects it needs, probably the very cleanest, resource-saving and testing-friendly way but I have doubts about the maintainability of this in the long run.

Most PHP frameworks I have looked into use either the singleton pattern, or functions that access the initialized objects. Both fine approaches, but as I said I'm happy with neither.

I would like to broaden my horizon on what common patterns exist here. I am looking for examples, additional ideas and pointers towards resources that discuss this from a long-term, real-world perspective.

Also, I'm interested to hear about specialized, niche or plain weird approaches to the issue.

 Answers

4

I would avoid the Singleton approach suggested by Flavius. There are numerous reasons to avoid this approach. It violates good OOP principles. The google testing blog has some good articles on the Singleton and how to avoid it:

http://googletesting.blogspot.com/2008/08/by-miko-hevery-so-you-join-new-project.html http://googletesting.blogspot.com/2008/05/tott-using-dependancy-injection-to.html http://googletesting.blogspot.com/2008/08/where-have-all-singletons-gone.html

Alternatives

  1. a service provider

    http://java.sun.com/blueprints/corej2eepatterns/Patterns/ServiceLocator.html

  2. dependency injection

    http://en.wikipedia.org/wiki/Dependency_injection

    and a php explanation:

    http://components.symfony-project.org/dependency-injection/trunk/book/01-Dependency-Injection

This is a good article about these alternatives:

http://martinfowler.com/articles/injection.html

Implementing dependency injection (DI):

  • I believe you should ask what is needed in the constructor for the object to function: new YourObject($dependencyA, $dependencyB);

  • You can provide the needed objects (dependencies) manually ($application = new Application(new MessageHandler()). But you can also use a DI framework (the wikipedia page provides links to PHP DI frameworks).

    Important is that you only pass in what you actually use (call an action on), NOT what you simply pass to other objects because they need it. Here's a recent post from 'uncle Bob' (Robert Martin) discussing manual DI vs using framework.

Some more thoughts on Flavius's solution. I don't want this post to be an anti-post but I think it's important to see why dependency injection is, at least for me, better than globals.

Even though it is not a 'true' Singleton implementation, I still think Flavius got it wrong. Global state is bad. Note that such solutions also use difficult to test static methods.

I know a lot of people do it, approve it and use it. But reading Misko Heverys blog articles (a google testability expert), rereading it and slowly digesting what he says did alter the way I see design a lot.

If you want to be able to test you application, you'll need to adopt a different approach to designing your application. When you do test-first programming, you'll have difficulty with things like this: 'next I want to implement logging in this piece of code; let's write a test first that logs a basic message' and then come up with a test that forces you to write and use a global logger that can't be replaced.

I am still struggling with all the information I got from that blog, and it's not always easy to implement, and I have many questions. But there's no way I can go back to what I did before (yes, global state and Singletons (big S)) after I grasped what Misko Hevery was saying :-)

Wednesday, August 3, 2022
2

Thanks Greg and Dave for the feedback. Wasn't quite what I was looking for, but I decided to put a bit of time into researching it and came up with this quick and dirty solution:

<?php
    $functions = array();
    $path = "/path/to/my/php/project";
    define_dir($path, $functions);
    reference_dir($path, $functions);
    echo
        "<table>" .
            "<tr>" .
                "<th>Name</th>" .
                "<th>Defined</th>" .
                "<th>Referenced</th>" .
            "</tr>";
    foreach ($functions as $name => $value) {
        echo
            "<tr>" . 
                "<td>" . htmlentities($name) . "</td>" .
                "<td>" . (isset($value[0]) ? count($value[0]) : "-") . "</td>" .
                "<td>" . (isset($value[1]) ? count($value[1]) : "-") . "</td>" .
            "</tr>";
    }
    echo "</table>";
    function define_dir($path, &$functions) {
        if ($dir = opendir($path)) {
            while (($file = readdir($dir)) !== false) {
                if (substr($file, 0, 1) == ".") continue;
                if (is_dir($path . "/" . $file)) {
                    define_dir($path . "/" . $file, $functions);
                } else {
                    if (substr($file, - 4, 4) != ".php") continue;
                    define_file($path . "/" . $file, $functions);
                }
            }
        }       
    }
    function define_file($path, &$functions) {
        $tokens = token_get_all(file_get_contents($path));
        for ($i = 0; $i < count($tokens); $i++) {
            $token = $tokens[$i];
            if (is_array($token)) {
                if ($token[0] != T_FUNCTION) continue;
                $i++;
                $token = $tokens[$i];
                if ($token[0] != T_WHITESPACE) die("T_WHITESPACE");
                $i++;
                $token = $tokens[$i];
                if ($token[0] != T_STRING) die("T_STRING");
                $functions[$token[1]][0][] = array($path, $token[2]);
            }
        }
    }
    function reference_dir($path, &$functions) {
        if ($dir = opendir($path)) {
            while (($file = readdir($dir)) !== false) {
                if (substr($file, 0, 1) == ".") continue;
                if (is_dir($path . "/" . $file)) {
                    reference_dir($path . "/" . $file, $functions);
                } else {
                    if (substr($file, - 4, 4) != ".php") continue;
                    reference_file($path . "/" . $file, $functions);
                }
            }
        }       
    }
    function reference_file($path, &$functions) {
        $tokens = token_get_all(file_get_contents($path));
        for ($i = 0; $i < count($tokens); $i++) {
            $token = $tokens[$i];
            if (is_array($token)) {
                if ($token[0] != T_STRING) continue;
                if ($tokens[$i + 1] != "(") continue;
                $functions[$token[1]][1][] = array($path, $token[2]);
            }
        }
    }
?>

I'll probably spend some more time on it so I can quickly find the files and line numbers of the function definitions and references; this information is being gathered, just not displayed.

Saturday, August 6, 2022
3

Dmitry Malyshenko answered what I needed to hear. "It's obvious that this token must be cached, not stored in configuration. How it would be cached - it totally up to you..."

Examples of valid solutions:

  • store in some file
  • store in a database
  • store just in memory (as a property of your class)
  • store in some key-value storage (redis, memcache)

I would have used CacheComponent but I am currently not developing in Symfony 3.1 so I decided to store in a database which felt most naturally. First I created a table with username, password and tokens in a mysql-database. Then I created a service dedicated to that table. It looks something like this simplified code:

class ExternalSiteService
{
    public function  getUsername() {
        ...
    }

    public function getPassword() {
        ...
    }

    public function getAccessToken() {
        ...
    }

    public function setAccessToken($newAccessToken) {
        ...
    }

    public function getRefreshToken() {
        $query="SELECT "
            . "eapi.refreshtoken "
            . "FROM external_api eapi";
        $connection = $this->em->getConnection();
        $statement = $connection->prepare($query);
        $statement->execute();
        $results = $statement->fetchAll();
        if ( $results ) {
            return $results;
        }
        return false;
    }

    public function setRefreshToken($newRefreshToken) {
        $query="UPDATE external_api "
            . "SET refreshtoken = :new_refreshtoken "
            . "WHERE id=1;";
        $connection = $this->em->getConnection();
        $statement = $connection->prepare($query);
        $statement->bindValue('new_refreshtoken', $newRefreshToken);
        $statement->execute();
    }
}

Then I made some logic in the controller that says if access token not valid use refresh token, and if refresh token not valid do a new login attempt. This code in the controller also stores the tokens in the database if we have to get new ones.

    $eapiService    = $this->get('mybundle.service.externalapi');
    $reqPara     = ['body' =>["accessToken" => $at, "department" => $department, "location" =>$location]];

    $req = $client->request("POST", "https://httpbin.org/employees/atwork", $reqPara);
    $response = json_decode($req->getBody()->getContents());

    if ($response->serverErrorMessage == "Invalid access token.") {
        $req = $client->request('POST', "https://httpbin.org/getnewtokens", ['body' => ['refreshToken' => $refreshToken]]);
        $response = json_decode($req->getBody()->getContents());

        if ($response->serverErrorMessage == "Invalid refresh token.") {
            $req = $client->request('POST', 'https://httpbin.org/login', ['body' => ['user' => $user, 'pw' => $pw]]);
            $response = json_decode($req->getBody()->getContents());
            foreach ($response->content as $contentItem) {
                $eapiService->setAccessToken($contentItem->accessToken);
                $eapiService->setRefreshToken($contentItem->refreshToken);
            }
        } else {
            foreach ($response->content as $contentItem) {
                $eapiService->setAccessToken($contentItem->accessToken);
                $eapiService->setRefreshToken($contentItem->refreshToken);
            }
        }
    }
    return array('usersatwork' => $response);

I will probably also make something to catch exceptions from guzzle.

Tuesday, August 16, 2022
4

I recommend Patterns of Enterprise Application Architecture by Martin Fowler.

A list of the patterns is also on his website

The DataMapper pattern is also relevant.

Sunday, August 14, 2022
2

Design patterns are supposed to provide a structure in which problems can be solved. When solving a real problem, you have to consider many tiny variations of a solution to that problem to see whether any fits a design pattern. In particular, you will probably need to generalise your problem, or its solution, in order to make a design pattern fit.

The answer is, it's an art. Knowing the design patterns is certainly an important step. One way to get used to this sort of thing is to study applications of design patterns, not just the patterns. Seeing many different applications of one pattern can help you over time to get better at mapping a task onto a pattern.

Thursday, November 17, 2022
Only authorized users can answer the search term. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :