Help me to use a Tom's PHP Framework, please

support
book

#1

I have read Tom's book, "PHP & Mysql: Novice to Ninja - Ed 6".

I'm "OK-ish" with the code: I get it... 90%.

I'm struggling with taking this framework and using it on my first "other" site. It's actually a redesign of an existing ASP.NET site and is not that complex. If anything, Tom's Joke site is more complex (and perhaps that's why I'm struggling - it's dumbing Tom's work down that is causing problems for me).

If I were to provide the basic requirements for the new site, I'd be really grateful if anyone could help just to point me in the right direction of how to begin bolting on a new project to the generic framework. Really grateful!

What follows is actually NOT the site I need to build but I'm using this example so that I can really simplify the design for you guys to understand it without having to read a LOT of code and description. The logic is exactly the same however.

Imagine I run a restaurant:

  • I employ 3 chefs: chefA, chefB, chefC.

  • I offer the usual 3 courses: starter, main, dessert.

  • I have customers who want to use the website to choose 3 courses by the SAME chef. (Yeah, odd in this context; not in the actual site I'm designing. Bear with me... :))

So, database tables:

"chefs"
chefId, chefName
1, chefA
2, chefB
3, chefC

"meals"
mealId, mealName
1, Avacado Toast
2, Prawn Cocktail
3, Sirloin Steak
4, Sea Bass
5, Banoffie Pie
6, Ice Cream

"chefMeal"
chefId, mealId
1, 1
2, 1
2, 2
3, 2
1, 3
1, 4
2, 3
3, 4
etc

Easy enough so far. Here's what I need:

I need to adjust Tom's Joke site so that my site has 3 navigation links in the navbar: Chef A, Chef B, Chef C
When the visitor clicks on Chef B, say, they get a submenu which looks something like this:

Chef A                              Chef B                              Chef C
  Starters                            Starters                             Starters
      Avacado Toast                   Avacado Toast                   Prawn Cocktail
                                              Prawn Cocktail
  Mains                              Mains                              Mains
      Sirloin Steak                     Sirloin Steak                    Sea Bass
      Sea Bass
  Desserts
etc...

(Any help with nice RESPONSIVE code for this menu would by great) BUT my main issue is... let's get back to Tom's framework/Ninja and project/Ijdb. I'm a bit messed up with what I need: I've got instances of DatabaseTable, I've got Controllers, I've got Entities, I've got Templates all going round and round in my head.

My site requires NO logging in. My site has NO permissions system. Everything is available to everybody.

OK, a long post. Apologies for being new to this and deeply confused. I'm not asking for huge amounts of code written for me. No, not at all. I'd just appreciate some pointers, some "if you don't need logins and permissions, strip that out by doing X, you WILL need (of all the things going around in your head, Y and Z).

Can anyone help a newbie?

Thank you.


Help with PHP & MySQL: Novice to Ninja - Cannot create class?
Help with PHP & MySQL: Novice to Ninja - Cannot create class?
#2

The first thing I see is that your meals are categorised (starters, mains and desserts), but there is no data for that in the database.
The next step would be to add that data.


#3

Are the any other interactions between visitors and the site aside from just viewing?

Sounds to me like the basis could be a single controller with two actions: a single page that just lists the chef, and another one that shows a list of all chefs plus the details of one of the Chefs.

Than you would have two templates and a partial, the partial being the menu. So the first page just includes the partial and shows only the menu, the second template includes the partial and has some logic to show details of the chef.

I'm not sure how DatabaseTable works in the book, so I can't comment on that. Maybe @TomB can help you out there :slight_smile:


#4

Hi Mike,

Let me clear something up first:

The database table structure you have provided does not account for the meal type (starter, main, dessert) which makes it impossible to generate the menu you provided, there is no way to currently know which meal is which type.

Firstly, we'll add a new table course:

id  name
1  Starters
2  Mains
3  Desserts

Then adjust the meals table table with a courseId column and fill in the relevant data.

If you set up entity classes for the various tables the same way the Joke class is set up in the book you should be able to do this:

$chefs = $chefsTable->findAll();
$courses = $courses->findAll();

foreach ($chefs as $chef) {
	
	foreach ($courses as $course) {
		echo $course->name;

		//get the corresponding records from the chefMeal table like the getAuthor method in the `Joke` class
		foreach ($chef->getMeals() as $chefMeal) {

			$meal = $mealsTable->findById($chefMeal->id);

			echo $meal->mealName;		



		}

	}
}

Your chef class would look something like this:

class Chef {
	private $chefMealTable;
	public $id;
	public $chefId;

	public function __construct(\Ninja\DatabaseTable $chefMealTable) {
		$this->chefMealTable = $chefMealTable;
	}

	public function getMeals() {
		return $this->chefMealTable->find('chefId', $this->chefId);
	}
}

That's the baisc premise, however, there's an issue here: This will display all the chef's meals under each course because the inner loop is just looping over each of the chef's meals

Chef A                 
  Starters             
      Avacado Toast    
      Sirloin Steak
      Sea Bass 
                       
  Mains                
      Avacado Toast    
      Sirloin Steak
      Sea Bass 
  Desserts
      Avacado Toast    
      Sirloin Steak
      Sea Bass

The simple but inefficient way to handle this is to just add an if statment:

$chefs = $chefsTable->findAll();
$courses = $courses->findAll();

foreach ($chefs as $chef) {
	
	foreach ($courses as $course) {

		//get the corresponding records from the chefMeal table like the getAuthor method in the `Joke` class
		foreach ($chef->getMeals() as $chefMeal) {
		         echo $course->name;
			$meal = $mealsTable->findById($chefMeal->id);

			if ($meal->courseId == $course->id) {
					echo $meal->mealName;	
			}


		}

	}
}

This is inefficient because it queries for all the related records in the chefMeals table once per course per chef when we only really should query the database once per chef. We are using all the meals so we do want to fetch all the chef's meals but only once. This is a simple fix, when getMeals() is called, check to see if the data has already been fetched, otherwise fetch it from the database. This is basic caching:

class Chef {
	private $chefMealTable;
	private $cachedMeals 
	public $id;
	public $chefId;

	public function __construct(\Ninja\DatabaseTable $chefMealTable) {
		$this->chefMealTable = $chefMealTable;
	}

	public function getMeals() {
		if (empty($this->cachedMeals)) {
			$this->cachedMeals = $this->chefMealTable->find('chefId', $this->chefId);
		}

		return $this->cachedMeals;
	}
}

With the cache in place, no matter how many times $chef->getMeals() is called, it will only query the database once.

And to tidy the code up further, you could make it so getMeals returns an array of meal objects already filtered by courseId (just by moving the inner loop into the function and giving the Chef entity access to the DatabaseTable instance for the meal table):

class Chef {
	private $chefMealTable;
	private $mealsTable;
	private $cachedMeals 
	public $id;
	public $chefId;

	public function __construct(\Ninja\DatabaseTable $chefMealTable, \Ninja\DatabaseTable $mealsTable) {
		$this->chefMealTable = $chefMealTable;
		$this->mealsTable = $mealsTable;
	}

	public function getMeals($courseId) {
		if (empty($this->cachedMeals)) {
			$this->cachedMeals = $this->chefMealTable->find('chefId', $this->chefId);
		}

		$meals = [];
		foreach ($this->cachedMeals as $chefMeal) {

			$meal = $this->mealsTable->findById($chefMeal->id);

			if ($meal->courseId == $course->id) {
				$meals[] = $meal;		
			}


		}
		return $meals;
	}
}

Then have simpler display logic

foreach ($chefs as $chef) {
	
	foreach ($courses as $course) {
		echo $course->name;

		//get the corresponding records from the chefMeal table like the getAuthor method in the `Joke` class
		foreach ($chef->getMeals($course->id) as $meal) {

			echo $meal->mealName;		


		}

	}
}

Hopefully that helps!


#5

Yes, apologies. You're quite right. Probably an extra column in "meals". Could go the full hog of another table ("setting" ?) with a mealSetting bridging table?


#6

Yes, SamA74 picked up on exactly the same thing. Yes, you're both right and that solution is what I would have done. Cheers.


#7

You could do this but only if you need the flexibility of a meal being in multiple courses.


#8

Hi, Tom.

Again, mate, thanks for the incredible detail of your response. That is SO helpful.

It's too much for a basic response so let me implement it and see how that goes then I'll get back to you with a meaningful "thanks".


#9

Hi, Tom.

I've had a go at this. Honestly, I wasn't sure WHERE to put the loop code but here's what I have done... and the error!

1.....
For this:
$chefs = $chefsTable->findAll();
$courses = $courses->findAll();

I added the following to IjdbRoutes:
private $chefsTable;
private $coursesTable;

constructor...

$this->chefsTable = new \Framework\DatabaseTable($pdo, 'chef', 'chefId', '\Its\Entity\Chef', 	[&$this->chefMealTable, &$this->mealsTable]);
$this->coursesTable = new \Framework\DatabaseTable($pdo, 'course', 'id', '\Its\Entity\Course');

I THINK I need to pick them up in EntryPoint->run() (See point 3 below)
$chefs = $this->chefsTable->findAll();
$courses = $this->coursesTable->findAll();

but this wont work. So, I added a 4th method to IjdbRoutes: (getNav()), like so:

public function getNav() {
	$nav = [
	'chefs' => $this->chefsTable->findAll(),
	'courses' => $this->coursesTable->findAll()
	];
	return $nav;
}

2.....
Then I created an entity for Chef like this:

<?php

namespace Ijdb\Entity;

class Chef {
	
	private $chefMealTable;
	private $mealsTable;
	private $cachedMeals;
	public $id;
	public $chefId;

	public function __construct(\Ninja\DatabaseTable $chefMealTable, \Ninja\DatabaseTable $mealsTable) {
		$this->chefMealTable = $chefMealTable;
		$this->mealsTable = $mealsTable;
	}

	public function getMeals($courseId) {
		if (empty($this->cachedMeals)) {
			$this->cachedMeals = $this->chefMealTable->find('chefId', $this->chefId);
		}
		$meals = [];
		foreach ($this->cachedMeals as $chefMeal) {
			$meal = $this->mealsTable->findById($chefMeal->id);
			if ($meal->courseId == $course->id) {
				$meals[] = $meal;		
			}
		}
		return $meals;
	}
	
}

3.....
I thought the nested foreach loops needed to be in master (or layout).html.php. This template is managed in EntryPoint.php not a Controller (as it's the "Master" page). So, I modified this like so:

echo $this->loadTemplate('layout.html.php', [
	'loggedIn' => $authentication->isLoggedIn(),
	'output' => $output,
	'title' => $title
]);

to this:

echo $this->loadTemplate('master.html.php', [
	'loggedIn' => $authentication->isLoggedIn(),
	'output' => $output,
	'title' => $title,
	'variables' => $this->routes->getNav()
]);

Hoping to pass $chefs and $courses into the template.

4.....
Then altered the nav in the master/layout template:

<ul>
    <li><a href="/">Home</a></li>
    
    <?php foreach ($chefs as $chef): ?>
        <li><?= $chef->name; ?>
	<?php foreach ($courses as $course): ?>
	    <li><?= $course->name; ?>
	        <?php foreach ($chef->getMeals($course->id) as $meal): ?>
	            	<li><?= $meal->mealName; ?>
	        <?php endforeach; ?>
	<?php endforeach; ?>
    <?php endforeach; ?>
    
    <?php if ($loggedIn): ?>
	<li><a href="/logout">Log out</a></li>
    <?php else: ?>
	<li><a href="/login">Log in</a></li>
    <?php endif; ?>
</ul>

ERROR...
SQLSTATE[HY000]: General error: could not call class constructor in /var/www/www.its-ltd.local/classes/Framework/DatabaseTable.php:132

Line 132 is the final line in the findAll() method, which reads:
return $result->fetchAll(\PDO::FETCH_CLASS, $this->className, $this->constructorArgs);

In the "process" (as shown by xDebug within NetBeans), this happens - as you've guessed - when EntryPoint->run() calls getNav() when loading the layout/master template.

Any ideas?

I'm not sure about my use of the references passed to the entity:
[&$this->chefMealTable, &$this->mealsTable]);
I don't think these are properly defined at all.

I also don't think I'm pulling out the 'variables' passed to the master template properly.

But, as I may be doing this ALL wrong from the outset. I thought I'd post where I am.


#10

UPDATE...

In order to do this in IjdbRoutes:

$this->chefsTable = new \Framework\DatabaseTable($pdo, 'chef', 'chefId', '\Its\Entity\Chef', [&$this->chefMealTable, &$this->mealsTable]);

I first need to instantiate:
$this->chefMealTable = new \Framework\DatabaseTable($pdo, 'chefMeal', 'chefId');
and
$this->mealsTable = new \Framework\DatabaseTable($pdo, 'meal', 'mealId');

I have done so. I doubt that the first will work however as it is a bridging (many-to-may) table and I doubt this can be instantiated using DatabaseTable (it has TWO primary keys, for example, and here I've only passed the name of the first as an argument).
But, I WOULD need to instantiate them if I wanted to use them as arguments for $this->chefsTable...

Obviously, I have added them as private variables to the IjdbRoutes class.

Thinking my getNav() should be in my Chef entity rather than IjdbRoutes??

ERROR:
I'm getting exactly the same error as stated in the last post.


#11

Hi, ScallioXTX
No, it’s just viewing. I see where you’re going with your guidance but this must fit into the Framework Tom has designed (or there’s been no point in me learning it) and this relies heavily on DatabaseTable. The findAll() method that I mention in my posts that is the point of the error is in DatabaseTable itself:

public function findAll($orderBy = null, $limit = null, $offset = null) {
		$query = 'SELECT * FROM ' . $this->table;
		if ($orderBy != null) {
			$query .= ' ORDER BY ' . $orderBy;
		}
		if ($limit != null) {
			$query .= ' LIMIT ' . $limit;
		}
		if ($offset != null) {
			$query .= ' OFFSET ' . $offset;
		}
		$result = $this->query($query);
		return $result->fetchAll(\PDO::FETCH_CLASS, $this->className, $this->constructorArgs);
	}

#13

Discussion continued at


#14