You will probably not get a complete answer on a forum, as this is a fairly large and complex topic. People are writing entire books on OOP best practices and design patterns, and many of us are still learning about these things while trying to develop and maintain existing projects.
I think you will find that Dependency Injection is hugely beneficial, even just on personal projects, at least this is my experience after I started using it. At first it is not very obvious, but it should become more obvious as you continue using it.
The thing is with OOP, it kinda forces you to consider the overall design of your application, and if you also follow DRY (Don't Repeat Yourself), then you will almost automatically force yourself into thinking about the design patterns.
Do not expect perfection from the start. It is hard avoid writing ineffective code while learning, but don't stop thinking about your solutions and best practices. You can always re-organize things as you learn.
What I personally found, is that inheritance (extends) are bad, because it couples your code and makes it harder to re-use. Instead, I am now using DI almost exclusively. Chances are that you will regret using "extends" because of following DRY, and when later realizing that you could re-use that code somewhere else. If you want to re-use a class that extends another class, then you are forced to instantiate the whole object. Not only is that ineffective, it is also not always possible, since some of that code might connect to a database or API which you do not need..
Another thing that is bad, which you pointed out yourself, is instantiating dependencies (classes) inside the class that relies on them. I used to do this for a very long time, thinking it made it easy to instantiate the classes without having to worry about instantiating the dependencies from my composition root. This is true, to a certain extent. But, it also creates a hard-coupling which makes it impractical to maintain and re-use the code as your project grows—even within your own personal project. So, no usage of new inside other classes, unless you use it in a factory class 🙂
When your dependencies themselves have dependencies, it will be impractical to instantiate them inside the constructors of classes, since you will have to update every single class if one of the dependencies changes its dependencies. Finally, it also takes more mental energy and time to assemble an object if you need to run tests on it.
If you must have code duplication (usually a code-smell), then you can also consider using traits. A trait is basically a copy/paste of code that is shared by multiple classes.
Now we are just discussing the code aspect, but your file system structure is just as important in keeping things simple. Hope this helps, and please note I am no expert, I am still learning about all this myself.