Skip to main content

๐Ÿƒโ€โ™‚๏ธ Application development setup

Lets start building our application building blocks:

  • .env file to store our environment variables
  • basic twig template
  • Document entities for :
    • Rental
    • Booking
    • Availability
  • Generate Data for the rentals collection

.env fileโ€‹

Create a .env file in the root of the project and add the following:

###> doctrine/mongodb-odm-bundle ###
MONGODB_URL=<your atlas cluster URI>
MONGODB_DB=symfony

Replace <your atlas cluster URI> with the connection string for your MongoDB Atlas cluster. You can find this in the MongoDB Atlas UI by clicking "Connect" and then "Connect your application".

Twig templateโ€‹

Twig is a template engine for PHP. It is the default template engine for Symfony. We will use it to create the views for our application. It is similar to other rendering templates which dynamically generate HTML, CSS, and JavaScript.

Create a base twig template in templates/base.html.twig:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}MongoDB Private Home aPartments Finder!{% endblock %}</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.png') }}">
<link href="{{ asset('css/styles.css') }}" rel="stylesheet">
<link href="https://use.fontawesome.com/releases/v5.15.3/css/all.css" rel="stylesheet">
{% block head %}{% endblock %}
</head>
<body>
<div class="header">
<a href="{{ path('rental_index') }}">
<img class="logo" src="{{ asset('php-logo.png') }}" alt="Private Home aPartments Logo"/>
</a>

<h1>Private Home aPartments</h1>
<a href="{{ path('booking_index') }}">My Bookings</a>
</div>
{% block body %}{% endblock %}
</body>
</html>

This template includes the Bootstrap CSS and a custom styles.css file. It also includes a header with a logo and a link to the rental index page. Every page in the application will extend this base template.

Add the following CSS to public/css/styles.css:

/* public/css/styles.css */

.rental-card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}

.rental-card h3 {
color: #333;
margin-top: 0;
}

.logo {
width: 150px;
height: 150px;
border-radius: 50%;
margin-bottom: 10px;
}

.header{
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background-color: #f1f1f1;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}

.date-pickers {
display: flex;
justify-content: space-between;
}

Additionally, add the public images to the public directory:

Document entitiesโ€‹

In src/Document, create the following document classes:

Rental.phpโ€‹

<?php
/**
* Rental
* ---------------------
* This class is responsible for defining the rental document and its properties.
*
* @category Document
* @package App\Document
* @author pavel.duchovny
* @license apache-2.0
*/

declare(strict_types=1);

namespace App\Document;

use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Doctrine\ODM\MongoDB\Types\Type;

/*
* Rental
---------------------
* This class is responsible for defining the rental document and its properties.
*/
#[
MongoDB\Document(collection: 'rentals')
]

class Rental
{
#[ODM\Id]
public $id;

#[ODM\Field(type: Type::STRING)]
public string $name;

#[ODM\Field(type: Type::STRING)]
public string $location;

#[ODM\Field(type: Type::INT)]
public int $nightCost;

#[ODM\EmbedMany(targetDocument: Availability::class)]
public Collection $availability;

/**
* __construct -
* This function is responsible for initializing the Rental class.
*
* @return void
*/
public function __construct()
{
$this->availability = new ArrayCollection();

// Create an instance of Availability and add it to the collection
$initialAvailability = new Availability();
$initialAvailability->startDate = new DateTime('2024-01-01');
$initialAvailability->endDate = new DateTime('2026-01-01');
$this->availability->add($initialAvailability);
}


/**
* Function : calcAvailabilitySlots
*
* This function is responsible for calculating the
* availability slots based on the booking start and end dates.
*
* @param DateTime $bookingStart - The booking start date
* @param DateTime $bookingEnd - The booking end date
* @param array $availability - The availability array
*
* @return ArrayCollection - The new availability slots
*/
public function calcAvailabilitySlots(DateTime $bookingStart,
DateTime $bookingEnd,
array $availability
): ArrayCollection {
// Create a new ArrayCollection to store the new availability
$newAvailability = new ArrayCollection();

// Loop through each period in the availability to calculate the new availability
foreach ($availability as $period) {
$periodStart = $period->startDate;
$periodEnd = $period->endDate;

// Booking is entirely before this period
if ($bookingEnd < $periodStart) {
$newAvailability->add($period);
continue;
}

// Booking is entirely after this period
if ($bookingStart > $periodEnd) {
$newAvailability->add($period);
continue;
}

// Booking starts before the period and ends within it
if ($bookingStart <= $periodStart && $bookingEnd < $periodEnd) {
$newPeriod = new Availability();
$newPeriod->startDate = $bookingEnd->modify('+1 day');
$newPeriod->endDate = $periodEnd;
$newAvailability->add($newPeriod);
continue;
}

// Booking starts during the period and ends after it
if ($bookingStart > $periodStart && $bookingEnd >= $periodEnd) {
$newPeriod = new Availability();
$newPeriod->startDate= $periodStart;
$newPeriod->endDate = $bookingStart->modify('-1 day');
$newAvailability->add($newPeriod);
continue;
}

// Booking is entirely within the period
if ($bookingStart > $periodStart && $bookingEnd < $periodEnd) {
$newPeriod1 = new Availability();
$newPeriod1->startDate = $periodStart;
$newPeriod1->endDate = $bookingStart->modify('-1 day');
$newAvailability->add($newPeriod1);

$newPeriod2 = new Availability();
$newPeriod2->startDate = $bookingEnd->modify('+1 day');
$newPeriod2->endDate = $periodEnd;
$newAvailability->add($newPeriod2);
continue;
}

// Booking covers the entire period
// Do not add the period to newAvailability (effectively removing it)
}

return $newAvailability;
}
}

Booking.phpโ€‹

<?php
/**
* Booking
* ---------------------
* This class is responsible for defining the booking document and its properties.
*
* @category Document
* @package App\Document
* @author pavel.duchovny
* @license apache-2.0
*/

declare(strict_types=1);

namespace App\Document;

use DateTime;
use DateTimeImmutable;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Doctrine\ODM\MongoDB\Types\Type;

/*
* Booking
---------------------
* This class is responsible for defining the booking document and its properties.
*/
#[
MongoDB\Document(collection: 'bookings')
]
class Booking
{
#[ODM\Id]
public $id;

#[ODM\ReferenceOne(targetDocument: Rental::class)]
public Rental $rental;

#[ODM\Field(type: Type::STRING)]
public string $rentalName;

#[ODM\Field(type: Type::STRING)]
public string $location;

#[ODM\Field(type: Type::INT)]
public int $totalCost;

#[ODM\Field(type: Type::DATE_IMMUTABLE)]
public DateTimeImmutable $startDate;

#[ODM\Field(type: Type::DATE_IMMUTABLE)]
public DateTimeImmutable $endDate;

// Add getters and setters for each property

}

Booking class exposes the properties of the booking document. It has a reference to the rental document, which is used to store the rental details for the booking.

Availability.phpโ€‹

<?php
/**
* Availability
*
* This class is responsible for defining the availability
* sub document and its properties.
*
* @category Document
* @package App\Document
* @author pavel.duchovny
* @license apache-2.0
*/

declare(strict_types=1);

namespace App\Document;

use DateTime;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Doctrine\ODM\MongoDB\Types\Type;
/*
* Availability
---------------------
* This class is responsible for defining the availability sub document and its properties.
*/

#[
ODM\EmbeddedDocument
]
class Availability
{
#[ODM\Field(type: Type::DATE)]
public DateTime $startDate;

#[ODM\Field(type: Type::DATE)]
public DateTime $endDate;


}

Those classes expose the properties of the rental, booking and availability documents. The Rental class has a collection of Availability subdocuments, which represent the availability of the rental. The only methods here are getters and setters for each property. Those will be used by the MongoDB ODM to map the documents to the database.

Generate Data for the rentals collection (Optional)โ€‹

  1. Use Atlas data explorer to upload the following rentals.json array into the "Insert Document" json array to upload initial rentals

  2. We can use the mongosh shell to insert some data into the rentals collection. We will also use the falso data generator to generate some random data for the rentals collection.

Prerequisitesโ€‹

  1. Node.js
  2. npm
  3. MongoDB Shell
  4. Falso
npm install @ngneat/falso
mongosh "mongodb+srv://<your atlas cluster URI>/test" --username <username>

Now we will run a script to generate some random data for the rentals collection. Copy the following script and run it in the mongosh shell:

const falso = require('@ngneat/falso');

// Switch to database named
use symfony;

const rentals = [];
for (let i = 0; i < 100; i++) {
rentals.push({
name: falso.randCompanyName(),
location: falso.randCity(),
night_cost: falso.randNumber({ min: 50, max: 500 }), // Random cost between 50 and 500
availability: [
{
start_date: new Date('2024-01-01'),
end_date: new Date('2026-01-01')
}
]
});
}

// Assuming your collection is named 'rentals'
db.rentals.insertMany(rentals);

// Create an index for searching by location
db.rentals.createIndex({ location: 1, "availability.start_date": 1, "availability.end_date": 1 });