In-Memory Specification Review0:00
We've already reviewed the fundamentals of the specification pattern, but we stopped just short of figuring out how that can translate to, for example, database queries. If we switch over to Sublime, here's the source code for that video. And you'll see we set up a repository that's essentially an in-memory collection. So you instantiate the repository and you pass through an array of customer objects. And then when we want to find all of those customers who match some kind of specification, in this case, we have it as simple as humanly possible. But anyways, if we want to find all customers who match that specification, well, here's basically what we were doing. We were fetching all the customers. In this case, it's not a big deal because it's in-memory. But then we loop over them and check to see if each one matches what we have here. And if so, we update the matches array and ultimately return.
it's not a big deal because it's N-memory. But then we loop over them and check to see if each one matches what we have here. And if so, we update the matches array and ultimately return that subset. Okay, so that definitely works for small collections. However, what about when, while you're not storing customers in memory, you're actually fetching them through a database query? Can we still use this pattern? And the answer is, absolutely. We just need to tweak some things a bit. So let's figure this out. First, I'm going to run my tests. But to do that, give me just a second and I will save a Sublime project. Honestly, you don't really need to worry about that at all. It has nothing to do with the code. And then next, I will create a phpunit.xml file just to make it a little easier to trigger our tests. Pretty basic and common stuff if you've ever used phpunit. Okay, so I'm going to run my tests directly from my editor. And sure enough,
Add Database Testing Setup1:27
file just to make it a little easier to trigger our tests. Pretty basic and common stuff if you've ever used phpunit. Okay, so I'm going to run my tests directly from my editor. And sure enough, we get green. Now we're going to tweak it. If we want to test this as if we were working with, for example, Eloquent, well, I don't have any kind of Laravel installation here. But we can still use the database component. Here's how. First, let's require illuminate/database. There we go. Next, within my tests, well, we're no longer working with an in-memory repository. So instead, we will replace this with, well, let's just go ahead and instantiate that. But again, we're going to query a database, but I don't have a database just yet. Let's set one up. First, I'm going to go ahead and import the Capsule manager. So let's see if I can pull this in. Yeah, and we will alias that to database. And then we'll say database equals a new Database.
Configure SQLite Schema2:17
I'm going to go ahead and import the Capsule manager. So let's see if I can pull this in. Yeah, and we will alias that to database. And then we'll say database equals a new Database. Next, we'll add a new connection. And this is very much like looking at your config/database.php file in a Laravel project. So database add connection. And all we want to do is test this in memory. So we can use SQLite, something like this. We'll say the driver here should be SQLite. And we can set a path to the database. Or we can always do this little trick here. That means we just want a database in memory. Next, we need a table, right? So let's set up a little migration here. Database Schema, create a customers table, where we will accept the table. This should look very familiar to any migration you've created. For example, a table increments the ID. And then we need another one. We'll keep this simple. A Customer just has a name,
This should look very familiar to any migration you've created. For example, a table increments the ID. And then we need another one. We'll keep this simple. A Customer just has a name, and then maybe a type. And then finally, maybe the timestamps. Okay, pretty simple stuff. Create a customers table. Next, why don't we extract some methods. For example, we'll call this migrateTables and create that and paste it in. Now, I need that database variable, and I could pass it through. Or a little trick, we could say $database set as global, and that'll make it available everywhere. And in fact, if you want this to feel a little bit more familiar, we could alias this, for example, to DB, like so. And now we can access it down here in a more traditional syntax that you might be familiar with. Okay, so maybe one more extract method. Here, we could call that setupDatabase, and then create that. All right, and I think that looks
traditional syntax that you might be familiar with. Okay, so maybe one more extract method. Here, we could call that setupDatabase, and then create that. All right, and I think that looks pretty good to me. And then later, if we want to extract some of this setup to a parent class, we could definitely do that as well. So the only final thing I see is, well, we need some customers to work with. Right now, yeah, we can have a customers table, but there's nothing in it. Now, we could use a factory tool. If you're using Laravel itself, then it has a factory builder straight out of the box. If you're working on a separate project with Eloquent, you could pull in testDummy that I built. Or in this case, I think we can keep it simple and just create some customers. However, if we want the functionality of Eloquent, then let's make sure we go to our Customer file and make sure it extends Eloquent. And remember, we can't just do that because we're
Seed Customers with Eloquent4:39
customers. However, if we want the functionality of Eloquent, then let's make sure we go to our customer file and make sure it extends Eloquent. And remember, we can't just do that because we're not in a Laravel project. So what we can do is pull in the Model class, and I will alias that to Eloquent. Okay, next, we can't do this anymore, so we can remove that entirely. And I will reformat. Okay, so if we switch back, we'll create some customers where name is Joe and type is gold. He's a gold customer. And then Jane is a silver customer. And we'll do that for each test. But remember, because it's a database in memory, it's not like you'll end up with two customers and two more in the next and two more. No, it's in memory. So every single time we're starting from scratch. Okay, so we had to do a little bit of setup there. But now we can move on to actually figuring out how to make this work. If we run our tests at the moment, of course, they're going to
scratch. Okay, so we had to do a little bit of setup there. But now we can move on to actually figuring out how to make this work. If we run our tests at the moment, of course, they're going to fail. Let's see what we have first. A Customer is gold if they have the respective type. So it looks like we need to switch over to our customerIsGold test. And sure enough, we have to replace this with the new syntax. All right, I will run the test again. And let's see, we have a mass assignment exception. If you're familiar with Eloquent, that should be pretty standard stuff. We can set the fillable or the guarded property. And we'll just set this to name as well as the type. Okay, let's run it again and see what we have to do next. Call to a member function connection on null. So that will be related to this. Let's see, we set up our capsule manager, we add the connection, we says global, and we forgot, I'm sorry, we forgot to boot Eloquent.
connection on null. So that will be related to this. Let's see, we set up our capsule manager, we add the connection, we says global, and we forgot, I'm sorry, we forgot to boot Eloquent. I'm going to run this again. And hopefully we're on to something else. And we are. So it fetches all customers. Argument one pass to the constructor must be a type array, but none given. Okay, so let's see what's up. Well, in our CustomerRepository, we're still expecting an array of customers. Remember, before, we were handling this in memory. So we're not going to do that anymore. Run it again. And let's see, it fetches all customers. So we're trying to do an assertCount, but that's not working. All right. Let's see. When we call an all method, we expect to have four customers. Well, first, we're only seeding it with two. So I will update that. And then next, we need to make that all method work.
When we call an all method, we expect to have four customers. Well, first, we're only seeding it with two. So I will update that. And then next, we need to make that all method work. No longer do we return that we return Customer::all(). Okay, let's run it again. And we're on to the next test. It fetches all customers who match a given specification. We asserted that the actual size one matches two. All right. So let's scroll down. No longer are we doing this. So I can remove that. And we can say results and then find the customers matching a specification where the customer is gold. Now, this time, we only have one. So I will assert that the count is one and run it again. And we get green. However, we're not done yet, are we? Because if we go back here, we're still in that same situation. If we want to find all customers who match a specification, this current implementation has us literally doing a select * from the table. And then we filter
Translate Specs to Query7:58
we're still in that same situation. If we want to find all customers who match a specification, this current implementation has us literally doing a select * from the table. And then we filter through the records in that table. It's fine if you only have 50 rows. But what about when you have 10,000 rows or 100,000 rows? That'll break down really quickly, right? So instead, it would be better if we can scope this on the query building end. Here's what we can do. I'm going to comment this out. And if we run our tests again, we should get red. And we do. And now, well, let's go ahead and get rid of all of it, in fact. And then we'll say customers. And let's go ahead and say customer. And we're going to set up a new query. And then I basically want to call this specification as if it was a local scope. So maybe we could say something like this. We already have our specification. And we already have that represented as a single piece of logic,
specification as if it was a local scope. So maybe we could say something like this. We already have our specification. And we already have that represented as a single piece of logic, right? Well, what if we could also have an additional way to represent this? For example, if we want to represent it as an eloquent scope, well, that would accept the query, right? And we would return query where the type is gold. And I'd say that's perfectly okay to do. Two different ways to represent the same sort of logic or expectation. So let's say customers equals specification, represent that as a scope, and then pass in our customers. And then finally, we can fetch all other results. Okay, let's run our test. And sweet, we get green. So we know it works. And of course, if you want to condense that into one line, you're certainly free to. But yeah, that's all there is to it. So now if I go back to my customer repository test, this is how you
Refine Repository API9:34
works. And of course, if you want to condense that into one line, you're certainly free to. But yeah, that's all there is to it. So now if I go back to my CustomerRepositoryTest, this is how you would reference it in your code. You have some specification that represents potentially a complicated piece of logic that is important to your domain and necessary for querying against. Okay, so we represent that as an object. And then we tell our repository, give me any of the records you have, which match this specification I give you. And in fact, if you want to make this a little more readable, maybe we could change this to whoMatch, get me the customers who match a customer's GoldSpecification. Okay, tests, of course, they fail. whoMatch, run it again, we get green. Okay, for this lesson, that'll do it.
