Repository: Qiskit/qiskit-terra (python)

Issue:
`'NoneType' object has no attribute 'metadata'` when trying to serialize custom gate on `qiskit-terra==0.21.2` when it used to work on `qiskit-terra==0.21.1`
### Environment

- **Qiskit Terra version**: 0.21.2
- **Python version**: 3.8.13
- **Operating system**: Ubuntu


### What is happening?

Qsikit errors out when trying to serial a circuit which contains a custom gate, when it used to work before on `qiskit-terra==0.21.1`

### How can we reproduce the issue?

Run 

```python
import io
from typing import Optional, Union

import numpy as np
import qiskit
from qiskit.circuit.library import XGate
from qiskit.qpy import dump

class CustomCXGate(qiskit.circuit.ControlledGate):
    def __init__(
        self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None
    ) -> None:
        super().__init__(
            "cx", 2, [], label, num_ctrl_qubits=1, ctrl_state=ctrl_state, base_gate=XGate()
        )

    def _define(self) -> None:
        qc = qiskit.QuantumCircuit(2, name=self.name)
        qc.cx(0, 1)
        self.definition = qc

    def __array__(self, dtype: Optional[type] = None) -> np.ndarray:
        mat = qiskit.circuit._utils._compute_control_matrix(
            self.base_gate.to_matrix(), self.num_ctrl_qubits, ctrl_state=self.ctrl_state
        )
        return np.asarray(mat, dtype=dtype)

circuit = qiskit.QuantumCircuit(2)
circuit.append(CustomCXGate(),[0, 1])
buf = io.BytesIO()
dump(circuit, buf)
```

This will throw an error

```
circuit.metadata, separators=(",", ":"), cls=metadata_serializer
AttributeError: 'NoneType' object has no attribute 'metadata'

```


### What should happen?

The above snippet should run with no errors.

### Any suggestions?

_No response_

Pull request description:
Fix qpy custom ControlledGate with overloaded _define()


<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary

This commit fixes a bug in the QPY serialization of ControlledGate subclasses that defined custom _define() methods. The _define() method is the typical way to provide a custom definition in Gate classes. While ControlledGate class provides an alternative interface that handles custom control states at initialization, but the _define() interface is still valid even if it doesn't account for this. In #8571 we updated the QPY serialization code to use the _definition() method directly to correctly handle the open control case. But this fix neglected the case where people were providing definitions via the mechanism from Gate. This commit fixes this assumption by ensuring we load the definition that comes from _define() which will fix the serialization of these custom subclasses.

### Details and comments

Fixes #8794

Patch:
diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py
index e42e3e346..075ac339b 100644
--- a/qiskit/qpy/binary_io/circuits.py
+++ b/qiskit/qpy/binary_io/circuits.py
@@ -632,6 +632,9 @@ def _write_custom_operation(file_obj, name, operation, custom_operations):
         # state is open, and the definition setter (during a subsequent read) uses the "fully
         # excited" control definition only.
         has_definition = True
+        # Build internal definition to support overloaded subclasses by
+        # calling definition getter on object
+        operation.definition  # pylint: disable=pointless-statement
         data = common.data_to_binary(operation._definition, write_circuit)
         size = len(data)
         num_ctrl_qubits = operation.num_ctrl_qubits
diff --git a/releasenotes/notes/fix-qpy-custom-controlled-gate-a9355df1a88a83a5.yaml b/releasenotes/notes/fix-qpy-custom-controlled-gate-a9355df1a88a83a5.yaml
new file mode 100644
index 000000000..528918211
--- /dev/null
+++ b/releasenotes/notes/fix-qpy-custom-controlled-gate-a9355df1a88a83a5.yaml
@@ -0,0 +1,10 @@
+---
+fixes:
+  - |
+    Fixed an issue in QPY serialization (:func:`~.qpy.dump`) when a custom
+    :class:`~.ControlledGate` subclass that overloaded the ``_define()``
+    method to provide a custom definition for the operation. Previously,
+    this case of operation was not serialized correctly because it wasn't
+    accounting for using the potentially ``_define()`` method to provide
+    a definition.
+    Fixes `#8794 <https://github.com/Qiskit/qiskit-terra/issues/8794>`__


Test patch:
diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py
index 5b576f690..22003420d 100644
--- a/test/python/circuit/test_circuit_load_from_qpy.py
+++ b/test/python/circuit/test_circuit_load_from_qpy.py
@@ -34,6 +34,7 @@ from qiskit.opflow import I, X, Y, Z
 from qiskit.test import QiskitTestCase
 from qiskit.circuit.qpy_serialization import dump, load
 from qiskit.quantum_info.random import random_unitary
+from qiskit.circuit.controlledgate import ControlledGate
 
 
 class TestLoadFromQPY(QiskitTestCase):
@@ -1055,6 +1056,34 @@ class TestLoadFromQPY(QiskitTestCase):
         new_circuit = load(qpy_file)[0]
         self.assertEqual(qc, new_circuit)
 
+    def test_controlled_gate_subclass_custom_definition(self):
+        """Test controlled gate with overloaded definition.
+
+        Reproduce from: https://github.com/Qiskit/qiskit-terra/issues/8794
+        """
+
+        class CustomCXGate(ControlledGate):
+            """Custom CX with overloaded _define."""
+
+            def __init__(self, label=None, ctrl_state=None):
+                super().__init__(
+                    "cx", 2, [], label, num_ctrl_qubits=1, ctrl_state=ctrl_state, base_gate=XGate()
+                )
+
+            def _define(self) -> None:
+                qc = QuantumCircuit(2, name=self.name)
+                qc.cx(0, 1)
+                self.definition = qc
+
+        qc = QuantumCircuit(2)
+        qc.append(CustomCXGate(), [0, 1])
+        qpy_file = io.BytesIO()
+        dump(qc, qpy_file)
+        qpy_file.seek(0)
+        new_circ = load(qpy_file)[0]
+        self.assertEqual(qc, new_circ)
+        self.assertEqual(qc.decompose(), new_circ.decompose())
+
     def test_load_with_loose_bits(self):
         """Test that loading from a circuit with loose bits works."""
         qc = QuantumCircuit([Qubit(), Qubit(), Clbit()])