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

support
book

#1

Discussion continued from

Hi, all.

I am banging my head against a brick wall with this one. I have previously posted on the issue but the thread has died and I have some new code/attempts to post anyway. I am up against a deadline with this one (probably shouldn’t have put too much faith in my ability to understand the framework deeply enough: should have built the new site without a framework and left using it until I’d had more time to fully understand it. But… I didn’t and now it’s going to take me longer to build without it than to struggle on and understand it. Retrospect - a wonderful thing!)

I am attempting to use the framework built throughout this book in my first “other” website. I am basically attempting to build a 3-level nav bar. Forget the layout (I can sort that myself), all I need is the data. Here goes:


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

“courses”
id, name
1 Stater
2 Main
3 Dessert

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 a chef they get a submenus which looks something like this: (all shown for clarity but, obviously, they’d only see one):

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…

That’s it.


Tom, the author of the book has very kindly offered the following which I have attempted to integrate into my website:

  1. Set up entity classes for the various tables the same way the Joke class is set up in the book. Your Chef class would like this:
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;
        }
}
  1. Then you should be able to do this:
$chefs = $chefsTable->findAll();
$courses = $courses->findAll(); // not sure if Tom meant $coursesTable here.

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;           


                }

        }
}

Looks like it makes perfect sense to me. Here’s what I’ve done so far to integrate this:

  1. I’ve created a “Chef” entity class, just like Tom’s above. I didn’t create any others (for the other tables) as I can’t see where, in Tom’s code, they are used.

  2. I modified the code in EntryPoint->run() which sends the layout.html.php template through the loadTemplate method like so. (I did this as this is where the navigation is. So, I’m basically creating the variables to pass into the template.)

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

… and here’s how I’ve modified the template to use the variables …

<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; ?>
			</ul>
  1. getNav() needs two instances of DatabaseTable (chefsTable and coursesTable), so I added the following code to IjdbRoutes:

a) Private Variables:

        private $chefMealTable; // needed for the arguments of chefsTable, below
	private $mealsTable; // ditto
	private $chefsTable;
	private $coursesTable;

b) Constructor…

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

c) new method

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

(I also added a line into \Framework\Routes for the new, getNav(), method, as it is actually the routing table (API) not IjdbRoutes that is passed into EntryPoint as an argument.

That’s it.


The error I’m getting is this:

“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 template.

The “class constructor” that it “could not call” is the Chef entity class.


I have tried and tried to work this out. It’s not a particularly useful error message.

Any help would be greatly appreciated.

If you think you can help but haven’t read the book, I am happy to post code that is “missing” from your understanding of the problem, just let me know.

Thanks.
Mike


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

Can you var_dump the variables used on line 132? Check that $this->className contains the name of the class and that $this->constructorArgs contains an array of two DatabaseTable instances.

It could be a really simple issue. The code I supplied assumed you’d named the framework Ninja like the book:

        public function __construct(\Ninja\DatabaseTable $chefMealTable, \Ninja\DatabaseTable $mealsTable) {

Indicating that two instances of Ninja\DatabaseTable are required. I can see from the rest of your code that you’re using the namespace Framework so you’ll need to adjust the constructor accordingly.


#6

Hi, Tom.

Thanks for getting back to me.

I cannot believe I did that (I’ve looked at this code again and again and missed that “Ninja”. OK, I’ve modified that so that it matches my use of “Framework” and that certainly was part of the issue, as the error message has changed.

Now, I’m getting:

  1. Undefined variable: chefs in /var/www/www.its-ltd.local/templates/layout.html.php on line 39
  2. Invalid argument supplied for foreach() in /var/www/www.its-ltd.local/templates/layout.html.php on line 39

Line 39 is this:
<?php foreach ($chefs as $chef): ?>
from the nested foreach loops above.

Basically, this code isn’t getting the correct data into the template:

$nav = $this->routes->getNav();

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

What do you think?


#7

Here are some screenshots (never used var_dump) from xDebug.

  1. Taken when program flow here:
    $nav = $this->routes->getNav();

  1. Taken when program flow here:
    extract($variables);
    on the layout template

Hope these help.


#8

Here, $chefs is an instance of the Chef entity class.

Doesn’t my way return an array of Chef entities? [0], [1], etc.

An extra level to dig down to in the layout template?

Argh???


#9

SOLVED


Hi, all.

I have solved this issue. There were two errors in “getMeals()”:

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->**mealId**);
			if ($meal->courseId == **$courseId**) {
				$meals[] = $meal;		
			}
		}
		return $meals;
	}

$course->id should have been $courseId
$chefMeal->id should have been chefMeal->mealId

Sorted now though.

Thanks to all who contributed, especially Tom who pointed out that I’d used Ninja instead of Framework in my namespace: duh!

So, this is a working 3-level, database-driven navbar using Tom’s framework. Let me know if you want to do the same and you have any questions.