Programming is a recursively deep field. From the outside it looks like it should be just one set of skills, but it's actually layers of skills, each layer built on the foundation of the last.
You need logical thought, the ability to break down big problems into smaller problems, the ability to search for information, and to apply the new information to your specific problem. And these are just the skills you need to get started. Every time you master the level you are at, you will find that a few more levels have opened up to you.
Becoming a master programmer is a pretty much endless journey, and it requires constant research, learning, effort, and focus. It takes time, practice, writing a lot of bad code, and actively seeking feedback on your code from more experienced programmers. It's not an easy road.
In my opinion, it's also one of the most rewarding fields of study. Every new idea that you internalize will increase your capacity. As a programmer, you can design mind castles with infinite robot workers to do anything that you want. The only limit to what you can build is what you can imagine. Becoming a better programmer enables you to build larger castles faster.
Beginner programmers mostly struggle with making their program do what they want it to. They have an idea in mind, but they don't know how to tell the computer to do it. The majority of their time is spent attempting to translate their thoughts into a list of steps that a computer can execute.
They will pull a lot of their code from Stack Overflow because they are struggling with the same problems that many beginners before them have already struggled with.
The best way to learn at the beginner stage is to focus on understanding your code. When you copy-paste from Stack Overflow, read it and play with it to understand why it does what it does. Read several answers and try to understand what makes them different, and why they all work. Once your program is functional, go back and make some guesses about what will happen when you change this line or that line. Test the changes and see if you were right. If you were, you have reinforced your understanding of your program. If you were wrong, then there is something else going on that you haven't considered yet. The key is to constantly challenge your assumptions to see if they hold up against reality, and to seek the new reality when they do not.
The journeyman programmer is competent in their language of choice. They understand the tools they are using, and they can learn new tools when they find a gap in their arsenal. When they have an job to do, they have a pretty good idea of how to accomplish it, or at least which direction to research. They can write code, they can find bugs, and they don't need a lot of hand-holding to get things done. Functionality has been conquered.
A journeyman will struggle with code reuse, the "proper place" for functionality, complex systems, and scalable systems.
To learn as a journeyman, you should try to identify patterns in your code, and extract those patterns into reusable modules. If you find yourself copy-pasting code around, that is an indication that there might be a broader idea hiding behind your code. Try to find it and pull it out.
In addition, set proper responsibility boundaries in your code (also known as "separation of concerns"). Your analytic code should never be responsible for activating users. Your user login code should not be responsible for picking a site theme. Your site theme code should not be responsible for anything security related. There is an instinct you will learn if you work at it. You can also use more experienced developers to help train this instinct in you.
I'm not going to pick on it too much, but typical PHP code serves as a great specific example here. I have seen a lot of PHP code where database queries are embedded in the middle of rendering some HTML to the browser. This is a clear violation of isolated responsibility. The visual code and the data logic are mixed together. A better way to do this would be to gather all the data at the beginning of the script, then render all the HTML at once. This way, you can detect an any errors that show up before you have already sent half the page to the browser.
Having an idea of what code should be responsible for what functionality is the core skill that allows a programmer to build large, maintainable systems. Once your code has clear responsibility boundaries, it will become more obvious where a bug is hiding and where you should add new functionality. Without this separation, every new change will likely add more bugs than features.
At the journeyman level, the way that you use Stack Overflow will change. You won't always find a clear answer to your problem. You will need to read a variety of answers to build up a mental model of how to solve your specific problem. You can also submit useful questions at this page, because you will be dealing with non-standard problems.
As a journeyman you should be actively seeking code reviews from more experienced developers. Your code will be functional, but there is always room to find a more effective approach, better responsibility separation, or catch subtle bugs before you push your code live.
As a journeyman, you will start to feel proud of your code. You will get defensive when someone criticizes your solution in any way. This is not a good thing.
You should absolutely feel proud of your work, but that should never prevent you from talking about your solution rationally. If you think your solution is actually the best way to go, and you can clearly state why, then make your case. But don't reject the advice just because "it's not the way I chose to do it". Even if you do end up rejecting the advice, spend some time considering the reviewer's points. There is usually at least a kernel of wisdom there that you can use to improve in the future.
This is worth repeating. Rationally discussing the merits of your solution is useful. General defensiveness is a waste of time. It will get in the way of your progress.
Ideally your reviewer should be able to clearly explain why their approach is better than yours. But even if they can't, just take their advice and try to see things from their perspective. Their "gut feeling" is probably more trained than yours. If you cling to the solutions you're fond of, you're missing an opportunity to grow.
An expert programmer will have no issue designing and implementing complex systems out of smaller parts. The subsystems will have good separation of concerns. The expert will know (or suspect) where their system is weak and where it is likely to break if the execution conditions change.
Moreover, an expert will measure their system to make sure that their intuition about bottlenecks is correct. By measuring before they make changes, they have confidence that they are actually solving the correct problem.
An expert's system will effectively use prebuilt technology because there is no good reason to re-implement the wheel when you're focused on Getting Things Done™. An expert will also be able to implement some new technology when necessary.
An expert will be comfortable creating reusable code. They should focus on making code that is easy to reuse, and easy to reuse correctly. They should watch for mistakes that they themselves and others make when trying to reuse their code, and applying that feedback to future versions.
The expert will be able to find bugs in their systems, because they understand what each section of the system is responsible for. Moreover, the expert will have strong opinions about what each section of the system should be responsible for.
Experts should be teachers; they should do code reviews with their teammates. In order to teach something effectively, you need to truly understand it yourself.
You do not really understand something unless you can explain it to your grandmother. – Albert Einstein
Even if you feel like you completely understand an idea, you'll understand it more deeply once you're able to explain it to a beginner.
An expert programmer can improve by always seeking the pattern behind the code. They should aim to develop a strong instinct for how a complex system breaks apart into smaller subsystems with clear responsibilities.
This is usually the point where some programmers begin to take on some team leadership. They have enough technical experience to quickly make correct decisions for themselves and for their team. They are effective themselves and can increase the effectiveness of those under them.
Leading a team is not necessary to advance as a programmer, but it's easier to get teaching experience if you do. You also have more leverage to institute code reviews and enforce code direction. And seeing what your team struggles with can help direct you to the types of problems that need to be broken down and made easier.
The line between expert and master is fuzzy. There is no single clear marker that separates the two. Broadly speaking, an expert is at the top of their field, and a master pushes the field forward.
A master programmer has a sharp instinct for when code "smells" wrong. This can be functionality in the wrong place, a feature that isn't needed, an incorrect solution to a problem, or a seemingly minor issue that will spread if not corrected.
Masters know when not to solve a problem. When a feature is out of scope for a project, or a good optimization isn't needed yet, a master knows when to say no, or not right now.
A master is concerned with momentum. They avoid solutions that will take a lot of effort to maintain, and they proactively solve "minor" problems that will grow like a cancer in a code base. They have an instinct for the future that allows them to steer clear of some problems before they ever become problems.
Master programmers should do lots of code review. Their programming instincts are hard won, so their opinion should carry a lot of weight.
Experience, subconscious pattern matching, and time spent thinking about complex problems will allow a master programmer to gain a deep understanding of their field. These often pave the way for a stroke of inspiration that allows them to make a hard problem easier, or to bypass the problem altogether.
A great example of this is the Express framework. It beautifully solves a specific problem (understandable, maintainable web servers), and the maintainers have a clear understanding of which functionality is out of scope for the project. New versions usually have less functionality than the previous, and the non-essential features are extracted into extensions because they are not important for the core problem.
Masters are fully familiar with the current state of expertise in their field, and they have the combination of experience, creativity, and critical thinking necessary to advance it.
The best way to learn as a master (or any level) is to sharpen yourself on other programmers. Have strong opinions, and know the reason your opinion is strong. Test your assumptions, and be willing to accept the truth when you are wrong. Have the wisdom to see when someone knows more than you do, and have the humility to learn what they have to offer. Most importantly, experiment frequently.
When you first learn to program, the gap between yourself and the masters seems like an uncrossable gulf. It feels like there is a group of minds that "has the stuff", and a separate group that doesn't (and clearly you and I are in that second group).
In reality, there is no "us and them". There is only the path and how far each individual has traveled. The masters have more experience finding the simplest way to express a complex idea. They have had better teachers or have spent more time exploring. And in some cases, their thought process just happens to be uniquely suited for they problem they're solving. But that doesn't mean they they are in some isolated club that you can never join. You can still become a very effective, master level programmer if you focus on experimenting and learning.
The Last Bit
Programming is a very deep, and very wide field. You could easily spend 10 lifetimes trying to comprehend all its knowledge across all its branches. But that's not the goal. There is a type of thinking that is common to programming as a whole, and that type of thinking can be learned. And it's worth the effort.