DEV Community

MongoDB Guests for MongoDB

Posted on • Edited on

MongoDB and PHP: Creating Your Own Framework

This content is written by Jack Woehr. You can find him on MongoDB Community Forum with the name Jack_Woehr.

TL;DR

We discuss creating your own domain-specific framework for a MongoDB-backed PHP web application. This article will provide a complete code demo of a Not Ready For Production issue tracker called Gesundheit. Get it? "Issue!" "Gesundheit!" (Sorry, dad joke.)

"Gesundheit" ("Guh ZOONT height"—you say it after someone sneezes) means "Health!" which is what you want for your applications.

"Why?!"

  • Because you learn more doing it this way.
  • Because over-reliance on other teams' tools is deadening to one's flair for phenomena essential for building solid code.
  • Because you should do it this way at least once.

MongoDB web applications—framework or no?

Eschew frameworks

As a minimalist, avoid a framework for a solution until you have coded a similar solution on your own at least once.

On a personal level, you learn more by doing it yourself once.

On a practical level, when you leverage a framework, you can run up against puzzles and problems that force you to delve into the framework code, which is easier if you have previously solved similar tasks to those handled by the framework.

You can’t cheat the learning curve!

Is it possible?

Frameworks provide a display metaphor to the front-end developer and a database abstraction to the back-end developer.

Open source languages for web development—Node.js, Python, and PHP—are increasingly integrated with frameworks.

Is it still possible to do meaningful development outside a framework?

“It depends…” If you want a breathtaking visual presentation and a state-of-the-art user interface, you’re probably committed to something like Vue.js.

On the other hand, if your concern is mostly the back end, and for the front end, you like mixing JavaScript with PHP and your database is MongoDB, it's easy enough to eschew frameworks and write your own.

Unique challenges of abstracting MongoDB

Abstracting relational database management system (RDBMS) code and various object databases lends itself readily to generalisation and abstraction. RDBMS practice has evolved standard relational object models coupled to declarative SQL, while object database entities are objects already.

MongoDB presents unique challenges. MongoDB code is procedural. Where SQL declares:

SELECT NAME, EMPNUM FROM EMPLOYEE ORDER BY EMPNUM
Enter fullscreen mode Exit fullscreen mode

The equivalent MongoDB code is procedural:

$result = mongodb_db->employee->aggregate([
         ['$project' => ['_id' => false,
            'name' => true,
            'empnum' => true]],
         ['$sort' => ['empnum' => 1]],
         ])->toArray();
Enter fullscreen mode Exit fullscreen mode

Compared to SQL, MongoDB code is less descriptive and more prescriptive.

… which has advantages …

SQL abstraction expresses itself in elegant queries that read like natural language and sit near the top of your code.

MongoDB abstraction involves encapsulating a complex, punctuation-heavy syntax in domain-specific building blocks. This encourages factoring early on in the development cycle.

MongoDB's prescriptive nature makes it more fluently parameterizable than SQL, both in addressing data and in choosing a code execution path.

Unique advantages of PHP + MongoDB

PHP is peculiar and peculiarly useful. It is baked into the World Wide Web and available on all platforms. It is mature, stable, secure, and progressive with a vibrant development team and a healthy user community.

PHP is an integral component of modernisation in my professional niche of IBM i enterprise modernization, and has a long and warm relationship with MongoDB (124 releases of the extension, 4367 commits!).

My business case … a digression you can skip

This is a digression: the story of a business-case-specific framework, how it grew, and how it went from PHP, MySQL, and light JavaScript to PHP, MongoDB, and heavy JavaScript. You can skip this story and jump ahead to "Hand-building a framework."

I married a ceramic artist

The website for my wife's pottery and ceramic art gallery started decades ago as straight HTML coded in Mozilla Composer, which was a WYSIWYG HTML editor. In January 2018, the website became interactive when we deployed a mixture of PHP, JavaScript, jQuery, and a tiny bit of Vue.js. The database was MySQL.

Make mine MongoDB

In 2020, the gallery website began to need a visual facelift. At that time, I became involved with MongoDB for professional reasons. I found the use case for MongoDB convincing enough to try switching out databases while working on coding the more sophisticated visual presentation.

Moving to MongoDB

I had already manually encapsulated MySQL in PHP factoring for the gallery website. I first wrote code to convert my tables one by one to MongoDB.

I started from a relational model which looked like this:

    MariaDB [website]> show tables;
    +--------------------+
    | Tables_in_website  |
    +--------------------+
    | Categorized       |
    | Category          |
    | Dirpath           |
    | Events            |
    | Exemplars         |
    | Image             |
    | ImageDetail       |
    | NonExemplars      |
    | Paypal            |
    | Pottery           |
    | Tags              |
    | UnCategorized     |
    +--------------------+
    12 rows in set (0.001 sec)
Enter fullscreen mode Exit fullscreen mode

… to a MongoDB exact representation that looked like this:

    website> show collections
    categorized     [view]
    category
    dirpath
    events
    image
    image_detail
    paypal
    pottery
    tags
    uncategorized   [view]
    system.views
Enter fullscreen mode Exit fullscreen mode

Two views, Exemplars and NonExemplars, were discarded in the process as unnecessary in the MongoDB direct translation of the original relational model.

I translated the extant MySQL encapsulation to MongoDB without changing the application semantics of the original member functions. The website now worked identically on top of either database, testing a PHP configuration variable to choose the code path.

Adopting MongoDB "made one thing clear"—that is, you don't need a relational schema to sell a piece of pottery. All you need is a document that has the pottery item's:

  • Name.
  • Pictures.
    • Really, references to file system locations. I don't store images in MongoDB.
  • Details and notes.
  • Price.
  • Buy button.

I updated the naively converted multi-collection MongoDB data model to a two-collection database and re-coded accordingly. As mongosh reveals, it's a much simpler model!

    newwebsite> show collections
    events
    pottery
Enter fullscreen mode Exit fullscreen mode

What was achieved in my use case by switching from an RDBMS to MongoDB

MariaDB/MySQL is a perfectly lovely RDBMS. I am fluent in SQL. However, as noted above, every relation in my use case could be expressed cleanly and naturally by embedding those relations in the individual MongoDB documents, at the same time dispensing with the complication of normal relational forms. If you want to understand more about relational algebra and relational calculus, the mathematical underpinnings of the relational database, see the works of C. J. Date.

In comparison with an RDBMS, MongoDB exacts some amount of extra execution detail from the coder while offering in return a simpler data model. The tradeoff was profitable in my use case.

Hand-building a framework

As mathematicians say, "2+2 equals 5, for sufficiently large values of 2." We have to define the term "framework." We're solving our problems, not the world's problems. Anselm Garbe once advised me not to sit down to write a framework, just sit down to write code that works and enhance it daily. The framework will grow of itself. And that's the spirit of this "framework": It is a toy example of tightly focused, domain-specific problem solving. In itself, it may or may not be extensible, but the approach is infinitely extensible.

Gesundheit

Our project is Gesundheit, a tiny, not production-ready(!!) issue tracker. The repository is on GitHub—you can stop reading here and go play with the code, or you can read on, or both.

Screenshot of Gesundheit Issue tracker

Gesundheit is not a model of ideal application architecture, nor of visual presentation, and definitely not a model of security (it's notably lacking, read on). It's a model of what one thinks about in designing an application-specific foundation for a MongoDB-based web application.

Security (lack of it)

Login security (insecurity!) in Gesundheit is plaintext-cookie-based with no attempt at concealment. The "toy" token in Gesundheit is merely the user name and the user number. The intention is to represent a security scheme while avoiding all discussion, comparison, and implementation of genuine security schemes. Security in a MongoDB-coded web application is a separate topic and provides the material for several long articles.

Suffice it to note that in a real application, you'll want to use token-based systems ranging from Basic Auth through PHP Sessions up to OAuth and the like.

PHP Level

PHP 8.x is required for the Gesundheit code.

How I started the project

I'm using Apache NetBeans. It's an open-source IDE that's been around for about 28 years. It's un-opinionated about PHP, while offering excellent PHP, CSS, and JavaScript editing features. You don't need NetBeans and can easily walk away from it after creating a project: NetBeans adds no dependencies on itself to projects.

NetBeans screenshot

Our project will run on our Ubuntu server via http in a subdirectory of the default server.

NetBeans screeenshot to set up the project

We're not choosing any frameworks!

Not selecting any framwwork while creating the project on NetBeans

NetBeans knows about Composer, so we'll let it install mongodb/mongodb in the vendor directory of the project. We have already installed the Mongodb extension via PECL as described by the PHP manual and mentioned by MongoDB language driver documentation.

New PHP Project wizard, Composer dialog: Choose mongodb/mongodb. Choose version 1.21.1

When I edit, I jump back and forth between the NetBeans editor and Visual Studio Code.

MongoDB database, collection, and validation

We'll use MongoDB Compass to create the database and an empty collection in our MongoDB Atlas cloud instance of MongoDB. We'll define our issue collection later in validation—we only name it here to MongoDB Atlas because Atlas can't create an empty database.

MongoDB Atlas

We use Node.js to create our validation, which effectively describes our document data model.

Why Node.js in this PHP article? Validators are deeply nested, and frankly, that kind of syntax is easier in Node.js than in PHP. Purists work harder than we lazy folk.

MongoDB Atlas does have a "Generate validation rules feature.” I'm used to writing validations (as in, "cut, paste, and edit") so I did the validator that way.

Creating validation rule

npm install mongodb
Enter fullscreen mode Exit fullscreen mode

The validator code is found in the repo in scripts/create_collections.js.

Example usage is in the comment block at the top of the collections.js file.

Model - View - Controller

Developers started talking about "MVC" (Model - View - Controller) architecture 35 to 40 years ago, early in the Smalltalk/X Windowing System/Macintosh/OS/2/Windows era. Back in the day, the data model, application control, and windowing activity usually were tangled together in the windowing event loop. The MVC pattern made it easier to maintain code. You no longer had (in theory) to dig into the visual code to find what was going on with the data model or control flow.

The way they do it

A typical framework in any web language has a router that responds to a URL request by invoking a controller on an HTML-plus-metalanguage template.

The way I do it

I've drifted from the original MVC design pattern as implemented by popular modern frameworks. No router. The URLs are absolute paths to top-level .php files which contain HTML and PHP. The top-level file calls members of the corresponding View. The View calls the Controller to manipulate the data supplied by the Models.

We start at the lower layers defining PHP classes for our framework. The PHP scripts serve the various URLs at the top level of the live application. Hand-coded JavaScript provides reactivity through most of the presentation with a "welcome" SPA coded in Vue.js.

Getting an overview of the code

You can clone/fork/browse the Gesundheit repository to get an overview of the code, or once you have cloned the code, you can follow the instructions in the README.md to generate code documentation using phpDocumentor and browse that documentation instead.

Layers of our full-stack, home-rolled framework

  • The model layer describes the data model and links it to MongoDB.
    • model/
      • DbModel.php
        • MongoDB code in PHP
      • IssueModel.php
        • Data model of an Issue
      • PostingModel.php
        • Data model of a comment posting in an Issue
      • UserModel.php
        • Data model of a User
  • The controller layer uses the data model to implement application logic.
    • controller/
      • IssueController.php
      • IssueEditController.php
      • LoginController.php
  • View layer uses the controllers to build interface components.
    • view/
      • IssueEditView.php
      • IssueView.php
      • LoginView.php
  • Utility routines are provided.
    • util/
      • Util.php
  • Top-level Application files mostly call on the views, with some reaching down to the model and controller layers, for example, to instance a DbModel for use with the upper layers.
    • PHP scripts
      • index.php
      • issueEdit.php
      • login_attempted.php
      • login.php
    • CSS
      • css/login.css
      • css/trackertable.css
    • Javascript deals with stuff PHP doesn't do well, like cookies.
      • js/js.cookie.min.js

A closer look at the layers

Models

DbModel

Aside from the boilerplate functions:

  • A constructor public function __construct()
  • A factory method, public static function newDbModel(): DbModel
  • A string method public function __toString(): string

DbModel connects and performs application-specific interactions with MongoDB, interacting with the user model, exposing methods such as:

  • public function get_all_userdocs(): array
  • public function get_userdoc_by_name(string $name): ?MongoDB\Model\BSONDocument
  • public function get_userdoc_by_usernum(int $usernum): ?MongoDB\Model\BSONDocument
  • public function upsert_userdoc(MongoDB\Model\BSONDocument $doc): bool
  • public function delete_user_by_name(string $name): bool

… and with the issue model exposing methods such as:

  • public function get_issue_by_issue_number(int $issue_number): ?MongoDB\Model\BSONDocument
  • public function upsert_issue(MongoDB\Model\BSONDocument $doc): bool

… including the fancy lookups (i.e., joins):

  • public function issue_user_lookup(int $issue_number): array
  • public function issue_user_lookup_all(): array

Judging by the MongoDB forums, it seems to me that users find $lookup to be the most difficult aggregation operator, so I made sure to use such an operation in this demo!

UserModel

UserModel describes users and creates user objects from and stores user objects to MongoDB using the functions provided by DbModel. Interestingly, the usernum field in the UserModel is not autoincrement. This is not a tutorial about how to implement autoincrement in MongoDB, which inherently lacks this facility. Learn more about autoincrement implementation.

IssueModel

IssueModel describes issues entered into the issue tracker and uses ConversationModel to represent the ongoing dialogue on an issue ticket.

Thoughts on class taxonomy

The framework here is application-specific. More than that, it's demo application-specific. There are aspects of the class layout that one would consider in greater depth if one were creating a serious application-specific framework or, beyond that, a generalised framework. The ideas here are relative to the code itself.

Class loading

Gesundheit is cavalier about class loading and does it in a fashion that's quick and practical. There may be redundancies, but that's why PHP offers require_once, right?! In any case, loading both model/Util.php and model/DbModel.php more or less starts the class load correctly in all cases.

Class design patterns

In coding Gesundheit, I surprised myself. Previous projects coded in this fashion have typically featured instances of the controllers and/or views whose __construct() methods require the database model as an argument. Instead, here there exist a few instances. Many static methods take the database model as an argument.

In small applications, this is easier to manage. The top-level presentation PHP code simply instantiates the DbModel and passes it to static methods. In a more complex application, it might be better to do it as I have done in production applications.

Division between Model, Controller, and View

In previous "roll your own" production code, I used both the "extends" inheritance design pattern and the "have-a" inheritance design pattern (class possesses an instance member of a foundation class) from the Controller, View, and Application layers to the Model layer and class inheritance from the View to the Controller. Gesundheit chooses a third way, which uses mostly methods to which instances of other classes are passed. This pattern, which is more like functional programming than object-oriented programming, offers the most clarity and the least implementation cost in the context. When you are creating your own, the design pattern you choose will vary with the circumstances.

JavaScript component

To delete cookies, I'm using js-cookie/js-cookie: A simple, lightweight JavaScript API for handling browser cookies downloaded from https://cdn.jsdelivr.net/npm/[email protected]/dist/js.cookie.min.js

Thoughts about the utilities

The static methods in Util.php fall into two categories:

  • "Plumbing" routines, such as dealing with dotenv
  • Rendering routines, turning complex sequences of HTML into function calls.

For the latter category, rendering routines: When I have used the "build your own" design pattern in production applications, I have pretty exhaustively applied them, to the point where there is no raw HTML in the top-level .php files. Here's some utility code from another application:

HTML File

"A good horse runs at the shadow of the whip": You get the idea from what I have provided. The code is easier to read, leaving much raw HTML structure interspersed with PHP.

Having learned from the past, Gesundheit's HTML-writing routines return the HTML as a string instead of directly outputting the HTML. That's a better design, because it's easier to combine function calls that way.

Installing the Gesundheit demo

Gesundheit runs easiest from your local Linux default Apache server instance (typically something like /var/www/html) in a Gesundheit subdirectory.

Essentially, all you have to do is copy the project (minus directories, like the scripts directory, that are only used in development) to your default web server HTML directory.

Put your .env file (modeled on the provided example.env file) above in the top level web server directory, e.g., /var/www/… the code expects to find the env two levels above, like this:

/var/www/
    .env
    html/
        Gesundheit/
Enter fullscreen mode Exit fullscreen mode

You'll need to create your MongoDB database and the collections. Use scripts/create_collections.js to create the collections with validation.

Use env.example to create a .env file.

There's also a script to create users in the scripts/ directory.

Automated unit testing (see the test directory) and documentation generation via phpDocumentor are provided for.

This is all covered exhaustively in the README.md in the Gesundheit repository.

Extending Gesundheit

  • A real issue tracker would, of course, have an admin interface to add users, and they would have user roles.
  • It would send email notifications to subscribers.
  • It would have date stamps.
  • There's always room for the unit tests.
    • I left off before writing the unit test case for creating a user.
  • I also tried to avoid commenting on the obvious.

Aside from all that, and from prettying things up with better stylesheets and layout, the most obvious extension is to create APIs so records can be updated without reloading a page. It's easy enough to write APIs that can be called from JavaScript, e.g., from a Vue.js single-page application.

Writing APIs for a MongoDB database in PHP… that's another article!

The moral of our fable

I've generally been able to accomplish my goals in software development. I believe it's because I've always started from the position that one must be able to code the whole stack by oneself before one starts relying on advanced tooling. If you don't know how to use a variety of hand saws, you'll find it difficult to learn to use a power saw.

You have that ability. Enjoy your learning path!

Top comments (0)